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

mendersoftware / inventory / 935169386

pending completion
935169386

Pull #396

gitlab-ci

merlin-northern
chore: many devices at once

Ticket: MEN-6425
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #396: feat: update inventory only when changed or outdated.

161 of 212 new or added lines in 3 files covered. (75.94%)

12 existing lines in 1 file now uncovered.

3253 of 3588 relevant lines covered (90.66%)

136.4 hits per line

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

86.9
/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
        "fmt"
20
        "reflect"
21
        "regexp"
22
        "strings"
23
        "time"
24

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

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

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

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

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

55
type DeviceID string
56

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

59
type GroupName string
60

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

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

73
func (da DeviceAttributes) GetByName(name string) *DeviceAttribute {
6✔
74
        for _, attribute := range da {
102✔
75
                if attribute.Name == name {
102✔
76
                        rc := attribute
6✔
77
                        return &rc
6✔
78
                }
6✔
79
        }
NEW
80
        return nil
×
81
}
82

83
func (da DeviceAttributes) ToMap(excludeNames ...string) map[string]DeviceAttribute {
34✔
84
        rc := make(map[string]DeviceAttribute, len(da))
34✔
85
        for _, v := range da {
124✔
86
                exclude := false
90✔
87
                for _, excluded := range excludeNames {
253✔
88
                        if v.Name == excluded {
197✔
89
                                exclude = true
34✔
90
                                break
34✔
91
                        }
92
                }
93
                if exclude {
124✔
94
                        continue
34✔
95
                }
96
                rc[v.Scope+"_"+v.Name] = v
56✔
97
        }
98
        return rc
34✔
99
}
100

101
func (da DeviceAttributes) Equal(existingAttributesArray DeviceAttributes) bool {
31✔
102
        // cannot be equal if they are of different size (the cheapest check first)
31✔
103
        if len(da)-2 != len(existingAttributesArray) { // -2 comes from updated and created timestamps
45✔
104
                return false
14✔
105
        }
14✔
106

107
        // cannot be equal if any of the values differ (the most common case)
108
        attributes := da.ToMap(AttrNameUpdated, AttrNameCreated)
17✔
109
        existingAttributes := existingAttributesArray.ToMap(AttrNameUpdated, AttrNameCreated)
17✔
110
        if len(attributes) != len(existingAttributes) {
18✔
111
                return false
1✔
112
        }
1✔
113
        for k, v := range attributes {
39✔
114
                if e, ok := existingAttributes[k]; ok {
46✔
115
                        if !v.Equal(e) {
24✔
116
                                return false
1✔
117
                        }
1✔
NEW
118
                } else {
×
NEW
119
                        // cannot be equal if keys differ (the last possibility)
×
NEW
120
                        return false
×
NEW
121
                }
×
122
        }
123

124
        return true
15✔
125
}
126

127
func (da DeviceAttribute) Validate() error {
2,142✔
128
        return validation.ValidateStruct(&da,
2,142✔
129
                validation.Field(&da.Name, validation.Required, validation.Length(1, 1024)),
2,142✔
130
                validation.Field(&da.Scope, validation.Required, validation.Length(1, 1024)),
2,142✔
131
                validation.Field(&da.Value, validation.By(validateDeviceAttrVal)),
2,142✔
132
                validation.Field(&da.Timestamp, validation.Date(time.RFC3339)),
2,142✔
133
        )
2,142✔
134
}
2,142✔
135

136
func allowedType(v reflect.Value) bool {
52✔
137
        if v.Type().Kind() == reflect.Int {
82✔
138
                return true
30✔
139
        }
30✔
140
        if v.Type().Kind() == reflect.String {
30✔
141
                return true
8✔
142
        }
8✔
143
        if v.Type().Kind() == reflect.Float64 {
22✔
144
                return true
8✔
145
        }
8✔
146
        return false
6✔
147
}
148

149
func reflectValuesEqual(rVal1 reflect.Value, rVal2 reflect.Value) bool {
15✔
150
        if rVal1.Len() != rVal2.Len() {
15✔
NEW
151
                return false
×
NEW
152
        }
×
153
        for i := 0; i < rVal1.Len(); i++ {
44✔
154
                //if !rVal1.Comparable() || !rVal1.Index(i).Equal(rVal2.Index(i)) { //does not work:
29✔
155
                //nolint:lll
29✔
156
                //model/device.go:149:14: rVal1.Comparable undefined (type reflect.Value has no field or method Comparable)
29✔
157
                //nolint:lll
29✔
158
                //model/device.go:149:46: rVal1.Index(i).Equal undefined (type reflect.Value has no field or method Equal)
29✔
159
                if rVal1.Index(i).Kind() != rVal2.Index(i).Kind() {
29✔
NEW
160
                        return false
×
NEW
161
                }
×
162
                if !allowedType(rVal1.Index(i)) || !allowedType(rVal2.Index(i)) {
35✔
163
                        if rVal1.Index(i) != rVal2.Index(i) {
6✔
NEW
164
                                return false
×
NEW
165
                        }
×
166
                } else {
23✔
167
                        if rVal2.Index(i).Kind() == reflect.Int {
38✔
168
                                value1 := rVal1.Index(i).Interface().(int)
15✔
169
                                value2 := rVal2.Index(i).Interface().(int)
15✔
170
                                if value1 != value2 {
16✔
171
                                        return false
1✔
172
                                }
1✔
173
                        }
174
                        if rVal2.Index(i).Kind() == reflect.String {
26✔
175
                                value1 := rVal1.Index(i).Interface().(string)
4✔
176
                                value2 := rVal2.Index(i).Interface().(string)
4✔
177
                                if value1 != value2 {
4✔
NEW
178
                                        return false
×
NEW
179
                                }
×
180
                        }
181
                        if rVal2.Index(i).Kind() == reflect.Float64 {
26✔
182
                                floatComparePrecision := "%.8f"
4✔
183
                                floatValue1 := rVal1.Index(i).Interface().(float64)
4✔
184
                                floatValue2 := rVal2.Index(i).Interface().(float64)
4✔
185
                                value1 := fmt.Sprintf(floatComparePrecision, floatValue1)
4✔
186
                                value2 := fmt.Sprintf(floatComparePrecision, floatValue2)
4✔
187
                                if value1 != value2 {
4✔
NEW
188
                                        return false
×
NEW
189
                                }
×
190
                        }
191
                }
192
        }
193
        return true
14✔
194
}
195

196
func (da DeviceAttribute) Equal(e DeviceAttribute) bool {
31✔
197
        if (da.Value == nil || e.Value == nil) && !(da.Value == nil && e.Value == nil) {
31✔
NEW
198
                return false
×
NEW
199
        }
×
200
        rVal1 := reflect.ValueOf(da.Value)
31✔
201
        rVal2 := reflect.ValueOf(e.Value)
31✔
202
        if rVal1.Kind() != rVal2.Kind() {
34✔
203
                if rVal1.Kind() == reflect.Slice && rVal2.Kind() == reflect.Array ||
3✔
204
                        rVal1.Kind() == reflect.Array && rVal2.Kind() == reflect.Slice {
4✔
205
                } else {
3✔
206
                        return false
2✔
207
                }
2✔
208
        }
209
        if rVal1.Kind() == reflect.Slice || rVal1.Kind() == reflect.Array {
44✔
210
                return reflectValuesEqual(rVal1, rVal2)
15✔
211
        } else {
29✔
212
                if da.Value != e.Value {
16✔
213
                        return false
2✔
214
                }
2✔
215
                if da.Scope == e.Scope {
24✔
216
                        if da.Name == e.Name {
24✔
217
                                if da.Description == nil && e.Description == nil {
24✔
218
                                        // at this point Value, Scope, and Name are equal, and both Descriptions are nil
12✔
219
                                        return true
12✔
220
                                }
12✔
NEW
221
                                if da.Description == nil || e.Description == nil {
×
NEW
222
                                        // if either is nil while the other is not they are different
×
NEW
223
                                        return false
×
NEW
224
                                }
×
NEW
225
                                if *da.Description == *e.Description {
×
NEW
226
                                        // both not nil, we can compare and if equal the attributes are equal
×
NEW
227
                                        return true
×
NEW
228
                                }
×
229
                        }
230
                }
231
        }
NEW
UNCOV
232
        return false
×
233
}
234

235
func validateDeviceAttrVal(i interface{}) error {
2,142✔
236
        if i == nil {
2,143✔
237
                return errors.New("supported types are string, float64, and arrays thereof")
1✔
238
        }
1✔
239
        rType := reflect.TypeOf(i)
2,141✔
240
        if rType.Kind() == reflect.Interface {
2,141✔
UNCOV
241
                rType = rType.Elem()
×
UNCOV
242
        }
×
243

244
        switch rType.Kind() {
2,141✔
245
        case reflect.Float64, reflect.String:
2,132✔
246
                return nil
2,132✔
247
        case reflect.Slice:
8✔
248
                elemKind := rType.Elem().Kind()
8✔
249
                if elemKind == reflect.Float64 || elemKind == reflect.String {
10✔
250
                        return nil
2✔
251
                } else if elemKind == reflect.Interface {
13✔
252
                        return validateDeviceAttrValArray(i)
5✔
253
                }
5✔
254
        }
255
        return errors.New("supported types are string, float64, and arrays thereof")
2✔
256
}
257

258
func validateDeviceAttrValArray(arr interface{}) error {
5✔
259
        rVal := reflect.ValueOf(arr)
5✔
260
        rLen := rVal.Len()
5✔
261
        if rLen == 0 {
6✔
262
                return nil
1✔
263
        }
1✔
264
        elem := rVal.Index(0)
4✔
265
        kind := elem.Kind()
4✔
266
        if elem.Kind() == reflect.Interface {
8✔
267
                elem = elem.Elem()
4✔
268
                kind = elem.Kind()
4✔
269
        }
4✔
270
        if kind != reflect.String && kind != reflect.Float64 {
5✔
271
                return errors.New(
1✔
272
                        "array values must be either string or float64, not: " +
1✔
273
                                kind.String())
1✔
274
        }
1✔
275
        for i := 1; i < rLen; i++ {
5✔
276
                elem = rVal.Index(i)
2✔
277
                elemKind := elem.Kind()
2✔
278
                if elemKind == reflect.Interface {
4✔
279
                        elemKind = elem.Elem().Kind()
2✔
280
                }
2✔
281
                if elemKind != kind {
3✔
282
                        return errors.New(
1✔
283
                                "array values must be of consistent type: " +
1✔
284
                                        "string or float64",
1✔
285
                        )
1✔
286
                }
1✔
287
        }
288
        return nil
2✔
289
}
290

291
// Device wrapper
292
type Device struct {
293
        //system-generated device ID
294
        ID DeviceID `json:"id" bson:"_id,omitempty"`
295

296
        //a map of attributes names and their values.
297
        Attributes DeviceAttributes `json:"attributes,omitempty" bson:"attributes,omitempty"`
298

299
        //device's group name
300
        Group GroupName `json:"-" bson:"group,omitempty"`
301

302
        CreatedTs time.Time `json:"-" bson:"created_ts,omitempty"`
303
        //Timestamp of the last attribute update.
304
        UpdatedTs time.Time `json:"updated_ts" bson:"updated_ts,omitempty"`
305

306
        //device object revision
307
        Revision uint `json:"-" bson:"revision,omitempty"`
308

309
        //tags attributes ETag
310
        TagsEtag string `json:"-" bson:"tags_etag,omitempty"`
311

312
        //text attribute for the full-text search
313
        Text string `json:"-" bson:"text,omitempty"`
314
}
315

316
// internalDevice is only used internally to avoid recursive type-loops for
317
// member functions.
318
type internalDevice Device
319

320
func (d *Device) UnmarshalBSON(b []byte) error {
217✔
321
        if err := bson.Unmarshal(b, (*internalDevice)(d)); err != nil {
217✔
UNCOV
322
                return err
×
UNCOV
323
        }
×
324
        for _, attr := range d.Attributes {
3,743✔
325
                if attr.Scope == AttrScopeSystem {
4,007✔
326
                        switch attr.Name {
481✔
327
                        case AttrNameGroup:
51✔
328
                                group := attr.Value.(string)
51✔
329
                                d.Group = GroupName(group)
51✔
330
                        case AttrNameUpdated:
215✔
331
                                dateTime := attr.Value.(primitive.DateTime)
215✔
332
                                d.UpdatedTs = dateTime.Time()
215✔
333
                        case AttrNameCreated:
215✔
334
                                dateTime := attr.Value.(primitive.DateTime)
215✔
335
                                d.CreatedTs = dateTime.Time()
215✔
336
                        }
337
                }
338
        }
339
        return nil
217✔
340
}
341

342
func (d Device) MarshalBSON() ([]byte, error) {
2✔
343
        if err := d.Validate(); err != nil {
2✔
UNCOV
344
                return nil, err
×
UNCOV
345
        }
×
346
        if d.Group != "" {
3✔
347
                d.Attributes = append(d.Attributes, DeviceAttribute{
1✔
348
                        Scope: AttrScopeSystem,
1✔
349
                        Name:  AttrNameGroup,
1✔
350
                        Value: d.Group,
1✔
351
                })
1✔
352
        }
1✔
353
        return bson.Marshal(internalDevice(d))
2✔
354
}
355

356
func (d Device) Validate() error {
78✔
357
        return validation.ValidateStruct(&d,
78✔
358
                validation.Field(&d.ID, validation.Required, validation.Length(1, 1024)),
78✔
359
                validation.Field(&d.Attributes),
78✔
360
                validation.Field(&d.TagsEtag, validation.Length(0, 1024)),
78✔
361
        )
78✔
362
}
78✔
363

364
func (did DeviceID) String() string {
305✔
365
        return string(did)
305✔
366
}
305✔
367

368
func (gn GroupName) String() string {
85✔
369
        return string(gn)
85✔
370
}
85✔
371

372
func (gn GroupName) Validate() error {
54✔
373
        if len(gn) > 1024 {
55✔
374
                return errors.New(
1✔
375
                        "Group name can at most have 1024 characters",
1✔
376
                )
1✔
377
        } else if len(gn) == 0 {
55✔
378
                return errors.New(
1✔
379
                        "Group name cannot be blank",
1✔
380
                )
1✔
381
        } else if !validGroupNameRegex.MatchString(string(gn)) {
54✔
382
                return errors.New(
1✔
383
                        "Group name can only contain: upper/lowercase " +
1✔
384
                                "alphanum, -(dash), _(underscore)",
1✔
385
                )
1✔
386
        }
1✔
387
        return nil
51✔
388
}
389

390
// wrapper for device attributes names and values
391
type DeviceAttributes []DeviceAttribute
392

393
func (d *DeviceAttributes) UnmarshalJSON(b []byte) error {
90✔
394
        err := json.Unmarshal(b, (*[]DeviceAttribute)(d))
90✔
395
        if err != nil {
90✔
UNCOV
396
                return err
×
UNCOV
397
        }
×
398
        for i := range *d {
1,173✔
399
                if (*d)[i].Scope == "" {
1,125✔
400
                        (*d)[i].Scope = AttrScopeInventory
42✔
401
                }
42✔
402
        }
403

404
        return nil
90✔
405
}
406

407
// MarshalJSON ensures that an empty array is returned if DeviceAttributes is
408
// empty.
409
func (d DeviceAttributes) MarshalJSON() ([]byte, error) {
65✔
410
        if d == nil {
66✔
411
                return json.Marshal([]DeviceAttribute{})
1✔
412
        }
1✔
413
        return json.Marshal([]DeviceAttribute(d))
64✔
414
}
415

416
func (d DeviceAttributes) Validate() error {
176✔
417
        for _, a := range d {
2,318✔
418
                if err := a.Validate(); err != nil {
2,147✔
419
                        return err
5✔
420
                }
5✔
421
        }
422
        return nil
171✔
423
}
424

425
func GetDeviceAttributeNameReplacer() *strings.Replacer {
3,357✔
426
        return strings.NewReplacer(".", string(runeDot), "$", string(runeDollar))
3,357✔
427
}
3,357✔
428

429
// UnmarshalBSONValue correctly unmarshals DeviceAttributes from Device
430
// documents stored in the DB.
431
func (d *DeviceAttributes) UnmarshalBSONValue(t bsontype.Type, b []byte) error {
217✔
432
        raw := bson.Raw(b)
217✔
433
        elems, err := raw.Elements()
217✔
434
        if err != nil {
217✔
UNCOV
435
                return err
×
436
        }
×
437
        *d = make(DeviceAttributes, len(elems))
217✔
438
        for i, elem := range elems {
3,743✔
439
                err = elem.Value().Unmarshal(&(*d)[i])
3,526✔
440
                if err != nil {
3,526✔
UNCOV
441
                        return err
×
UNCOV
442
                }
×
443
        }
444

445
        return nil
217✔
446
}
447

448
// MarshalBSONValue marshals the DeviceAttributes to a mongo-compatible
449
// document. That is, each attribute is given a unique field consisting of
450
// "<scope>-<name>".
451
func (d DeviceAttributes) MarshalBSONValue() (bsontype.Type, []byte, error) {
2✔
452
        attrs := make(bson.D, len(d))
2✔
453
        replacer := GetDeviceAttributeNameReplacer()
2✔
454
        for i := range d {
7✔
455
                attr := DeviceAttribute{
5✔
456
                        Name:        d[i].Name,
5✔
457
                        Description: d[i].Description,
5✔
458
                        Value:       d[i].Value,
5✔
459
                        Scope:       d[i].Scope,
5✔
460
                        Timestamp:   d[i].Timestamp,
5✔
461
                }
5✔
462
                attrs[i].Key = attr.Scope + "-" + replacer.Replace(d[i].Name)
5✔
463
                attrs[i].Value = &attr
5✔
464
        }
5✔
465
        return bson.MarshalValue(attrs)
2✔
466
}
467

468
type DeviceUpdate struct {
469
        Id       DeviceID `json:"id"`
470
        Revision uint     `json:"revision"`
471
}
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