• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-server / 1622978334

13 Jan 2025 03:51PM UTC coverage: 72.802% (-3.8%) from 76.608%
1622978334

Pull #300

gitlab-ci

alfrunes
fix: Deployment device count should not exceed max devices

Added a condition to skip deployments when the device count reaches max
devices.

Changelog: Title
Ticket: MEN-7847
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #300: fix: Deployment device count should not exceed max devices

4251 of 6164 branches covered (68.96%)

Branch coverage included in aggregate %.

0 of 18 new or added lines in 1 file covered. (0.0%)

2544 existing lines in 83 files now uncovered.

42741 of 58384 relevant lines covered (73.21%)

21.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.63
/backend/services/inventory/model/device.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package model
16

17
import (
18
        "encoding/json"
19
        "reflect"
20
        "regexp"
21
        "strings"
22
        "time"
23

24
        validation "github.com/go-ozzo/ozzo-validation/v4"
25
        "github.com/pkg/errors"
26
        "go.mongodb.org/mongo-driver/bson"
27
        "go.mongodb.org/mongo-driver/bson/bsontype"
28
        "go.mongodb.org/mongo-driver/bson/primitive"
29
)
30

31
const (
32
        AttrScopeInventory = "inventory"
33
        AttrScopeIdentity  = "identity"
34
        AttrScopeSystem    = "system"
35
        AttrScopeTags      = "tags"
36
        AttrScopeMonitor   = "monitor"
37

38
        AttrNameID             = "id"
39
        AttrNameGroup          = "group"
40
        AttrNameUpdated        = "updated_ts"
41
        AttrNameCreated        = "created_ts"
42
        AttrNameTagsEtag       = "tags_etag"
43
        AttrNameNumberOfAlerts = "alert_count"
44
        AttrNameAlerts         = "alerts"
45
)
46

47
const (
48
        runeDollar = '\uFF04'
49
        runeDot    = '\uFF0E'
50
)
51

52
var validGroupNameRegex = regexp.MustCompile("^[A-Za-z0-9_-]*$")
53

54
type DeviceID string
55

56
var NilDeviceID DeviceID //TODO: how to make it NilDeviceID:=DeviceID(primitive.NilObjectID)
57

58
type GroupName string
59

60
type DeviceGroups struct {
61
        Groups []string `json:"groups" bson:"-"`
62
}
63

64
type DeviceAttribute struct {
65
        Name        string      `json:"name" bson:",omitempty"`
66
        Description *string     `json:"description,omitempty" bson:",omitempty"`
67
        Value       interface{} `json:"value" bson:",omitempty"`
68
        Scope       string      `json:"scope" bson:",omitempty"`
69
        Timestamp   *time.Time  `json:"timestamp,omitempty" bson:",omitempty"`
70
}
71

72
func (da DeviceAttribute) Validate() error {
1✔
73
        return validation.ValidateStruct(&da,
1✔
74
                validation.Field(&da.Name, validation.Required, validation.Length(1, 1024)),
1✔
75
                validation.Field(&da.Scope, validation.Required, validation.Length(1, 1024)),
1✔
76
                validation.Field(&da.Value, validation.By(validateDeviceAttrVal)),
1✔
77
                validation.Field(&da.Timestamp, validation.Date(time.RFC3339)),
1✔
78
        )
1✔
79
}
1✔
80

81
func validateDeviceAttrVal(i interface{}) error {
1✔
82
        if i == nil {
2✔
83
                return errors.New("supported types are string, float64, and arrays thereof")
1✔
84
        }
1✔
85
        rType := reflect.TypeOf(i)
1✔
86
        if rType.Kind() == reflect.Interface {
1✔
87
                rType = rType.Elem()
×
88
        }
×
89

90
        switch rType.Kind() {
1✔
91
        case reflect.Float64, reflect.String:
1✔
92
                return nil
1✔
93
        case reflect.Slice:
1✔
94
                elemKind := rType.Elem().Kind()
1✔
95
                if elemKind == reflect.Float64 || elemKind == reflect.String {
2✔
96
                        return nil
1✔
97
                } else if elemKind == reflect.Interface {
3✔
98
                        return validateDeviceAttrValArray(i)
1✔
99
                }
1✔
100
        }
101
        return errors.New("supported types are string, float64, and arrays thereof")
1✔
102
}
103

104
func validateDeviceAttrValArray(arr interface{}) error {
1✔
105
        rVal := reflect.ValueOf(arr)
1✔
106
        rLen := rVal.Len()
1✔
107
        if rLen == 0 {
2✔
108
                return nil
1✔
109
        }
1✔
110
        elem := rVal.Index(0)
1✔
111
        kind := elem.Kind()
1✔
112
        if elem.Kind() == reflect.Interface {
2✔
113
                elem = elem.Elem()
1✔
114
                kind = elem.Kind()
1✔
115
        }
1✔
116
        if kind != reflect.String && kind != reflect.Float64 {
2✔
117
                return errors.New(
1✔
118
                        "array values must be either string or float64, not: " +
1✔
119
                                kind.String())
1✔
120
        }
1✔
121
        for i := 1; i < rLen; i++ {
2✔
122
                elem = rVal.Index(i)
1✔
123
                elemKind := elem.Kind()
1✔
124
                if elemKind == reflect.Interface {
2✔
125
                        elemKind = elem.Elem().Kind()
1✔
126
                }
1✔
127
                if elemKind != kind {
2✔
128
                        return errors.New(
1✔
129
                                "array values must be of consistent type: " +
1✔
130
                                        "string or float64",
1✔
131
                        )
1✔
132
                }
1✔
133
        }
134
        return nil
1✔
135
}
136

137
// Device wrapper
138
type Device struct {
139
        //system-generated device ID
140
        ID DeviceID `json:"id" bson:"_id,omitempty"`
141

142
        //a map of attributes names and their values.
143
        Attributes DeviceAttributes `json:"attributes,omitempty" bson:"attributes,omitempty"`
144

145
        //device's group name
146
        Group GroupName `json:"-" bson:"group,omitempty"`
147

148
        CreatedTs time.Time `json:"-" bson:"created_ts,omitempty"`
149
        //Timestamp of the last attribute update.
150
        UpdatedTs *time.Time `json:"updated_ts,omitempty" bson:"updated_ts,omitempty"`
151

152
        //device object revision
153
        Revision uint `json:"-" bson:"revision,omitempty"`
154

155
        //tags attributes ETag
156
        TagsEtag string `json:"-" bson:"tags_etag,omitempty"`
157

158
        //text attribute for the full-text search
159
        Text string `json:"-" bson:"text,omitempty"`
160
}
161

162
// internalDevice is only used internally to avoid recursive type-loops for
163
// member functions.
164
type internalDevice Device
165

166
func (d *Device) UnmarshalBSON(b []byte) error {
1✔
167
        if err := bson.Unmarshal(b, (*internalDevice)(d)); err != nil {
1✔
168
                return err
×
169
        }
×
170
        for _, attr := range d.Attributes {
2✔
171
                if attr.Scope == AttrScopeSystem {
2✔
172
                        switch attr.Name {
1✔
173
                        case AttrNameGroup:
1✔
174
                                group := attr.Value.(string)
1✔
175
                                d.Group = GroupName(group)
1✔
UNCOV
176
                        case AttrNameUpdated:
×
UNCOV
177
                                if attr.Value != nil {
×
UNCOV
178
                                        dateTime := attr.Value.(primitive.DateTime).Time()
×
UNCOV
179
                                        d.UpdatedTs = &dateTime
×
UNCOV
180
                                } else {
×
181
                                        d.UpdatedTs = nil
×
182
                                }
×
UNCOV
183
                        case AttrNameCreated:
×
UNCOV
184
                                dateTime := attr.Value.(primitive.DateTime)
×
UNCOV
185
                                d.CreatedTs = dateTime.Time()
×
186
                        }
187
                }
188
        }
189
        return nil
1✔
190
}
191

192
func (d Device) MarshalBSON() ([]byte, error) {
1✔
193
        if err := d.Validate(); err != nil {
1✔
194
                return nil, err
×
195
        }
×
196
        if d.Group != "" {
2✔
197
                d.Attributes = append(d.Attributes, DeviceAttribute{
1✔
198
                        Scope: AttrScopeSystem,
1✔
199
                        Name:  AttrNameGroup,
1✔
200
                        Value: d.Group,
1✔
201
                })
1✔
202
        }
1✔
203
        return bson.Marshal(internalDevice(d))
1✔
204
}
205

206
func (d Device) Validate() error {
1✔
207
        return validation.ValidateStruct(&d,
1✔
208
                validation.Field(&d.ID, validation.Required, validation.Length(1, 1024)),
1✔
209
                validation.Field(&d.Attributes),
1✔
210
                validation.Field(&d.TagsEtag, validation.Length(0, 1024)),
1✔
211
        )
1✔
212
}
1✔
213

UNCOV
214
func (did DeviceID) String() string {
×
UNCOV
215
        return string(did)
×
UNCOV
216
}
×
217

UNCOV
218
func (gn GroupName) String() string {
×
UNCOV
219
        return string(gn)
×
UNCOV
220
}
×
221

222
func (gn GroupName) Validate() error {
1✔
223
        if len(gn) > 1024 {
2✔
224
                return errors.New(
1✔
225
                        "Group name can at most have 1024 characters",
1✔
226
                )
1✔
227
        } else if len(gn) == 0 {
3✔
228
                return errors.New(
1✔
229
                        "Group name cannot be blank",
1✔
230
                )
1✔
231
        } else if !validGroupNameRegex.MatchString(string(gn)) {
3✔
232
                return errors.New(
1✔
233
                        "Group name can only contain: upper/lowercase " +
1✔
234
                                "alphanum, -(dash), _(underscore)",
1✔
235
                )
1✔
236
        }
1✔
237
        return nil
1✔
238
}
239

240
// wrapper for device attributes names and values
241
type DeviceAttributes []DeviceAttribute
242

243
func (d *DeviceAttributes) UnmarshalJSON(b []byte) error {
1✔
244
        err := json.Unmarshal(b, (*[]DeviceAttribute)(d))
1✔
245
        if err != nil {
1✔
246
                return err
×
247
        }
×
248
        for i := range *d {
2✔
249
                if (*d)[i].Scope == "" {
2✔
250
                        (*d)[i].Scope = AttrScopeInventory
1✔
251
                }
1✔
252
        }
253

254
        return nil
1✔
255
}
256

257
// MarshalJSON ensures that an empty array is returned if DeviceAttributes is
258
// empty.
259
func (d DeviceAttributes) MarshalJSON() ([]byte, error) {
1✔
260
        if d == nil {
2✔
261
                return json.Marshal([]DeviceAttribute{})
1✔
262
        }
1✔
263
        return json.Marshal([]DeviceAttribute(d))
1✔
264
}
265

266
func (d DeviceAttributes) Validate() error {
1✔
267
        for _, a := range d {
2✔
268
                if err := a.Validate(); err != nil {
2✔
269
                        return err
1✔
270
                }
1✔
271
        }
272
        return nil
1✔
273
}
274

275
func GetDeviceAttributeNameReplacer() *strings.Replacer {
1✔
276
        return strings.NewReplacer(".", string(runeDot), "$", string(runeDollar))
1✔
277
}
1✔
278

279
// UnmarshalBSONValue correctly unmarshals DeviceAttributes from Device
280
// documents stored in the DB.
281
func (d *DeviceAttributes) UnmarshalBSONValue(t bsontype.Type, b []byte) error {
1✔
282
        raw := bson.Raw(b)
1✔
283
        elems, err := raw.Elements()
1✔
284
        if err != nil {
1✔
285
                return err
×
286
        }
×
287
        *d = make(DeviceAttributes, len(elems))
1✔
288
        for i, elem := range elems {
2✔
289
                err = elem.Value().Unmarshal(&(*d)[i])
1✔
290
                if err != nil {
1✔
291
                        return err
×
292
                }
×
293
        }
294

295
        return nil
1✔
296
}
297

298
// MarshalBSONValue marshals the DeviceAttributes to a mongo-compatible
299
// document. That is, each attribute is given a unique field consisting of
300
// "<scope>-<name>".
301
func (d DeviceAttributes) MarshalBSONValue() (bsontype.Type, []byte, error) {
1✔
302
        attrs := make(bson.D, len(d))
1✔
303
        replacer := GetDeviceAttributeNameReplacer()
1✔
304
        for i := range d {
2✔
305
                attr := DeviceAttribute{
1✔
306
                        Name:        d[i].Name,
1✔
307
                        Description: d[i].Description,
1✔
308
                        Value:       d[i].Value,
1✔
309
                        Scope:       d[i].Scope,
1✔
310
                        Timestamp:   d[i].Timestamp,
1✔
311
                }
1✔
312
                attrs[i].Key = attr.Scope + "-" + replacer.Replace(d[i].Name)
1✔
313
                attrs[i].Value = &attr
1✔
314
        }
1✔
315
        return bson.MarshalValue(attrs)
1✔
316
}
317

318
type DeviceUpdate struct {
319
        Id       DeviceID `json:"id"`
320
        Revision uint     `json:"revision"`
321
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc