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

mendersoftware / inventory / 1146958214

22 Jan 2024 04:34PM UTC coverage: 91.403% (-0.4%) from 91.79%
1146958214

push

gitlab-ci

web-flow
Merge pull request #438 from kjaskiewiczz/men-6878

feat: do not set updated_ts field when inserting the device

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

1 existing line in 1 file now uncovered.

3094 of 3385 relevant lines covered (91.4%)

149.03 hits per line

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

89.94
/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 {
2,142✔
73
        return validation.ValidateStruct(&da,
2,142✔
74
                validation.Field(&da.Name, validation.Required, validation.Length(1, 1024)),
2,142✔
75
                validation.Field(&da.Scope, validation.Required, validation.Length(1, 1024)),
2,142✔
76
                validation.Field(&da.Value, validation.By(validateDeviceAttrVal)),
2,142✔
77
                validation.Field(&da.Timestamp, validation.Date(time.RFC3339)),
2,142✔
78
        )
2,142✔
79
}
2,142✔
80

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

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

104
func validateDeviceAttrValArray(arr interface{}) error {
5✔
105
        rVal := reflect.ValueOf(arr)
5✔
106
        rLen := rVal.Len()
5✔
107
        if rLen == 0 {
6✔
108
                return nil
1✔
109
        }
1✔
110
        elem := rVal.Index(0)
4✔
111
        kind := elem.Kind()
4✔
112
        if elem.Kind() == reflect.Interface {
8✔
113
                elem = elem.Elem()
4✔
114
                kind = elem.Kind()
4✔
115
        }
4✔
116
        if kind != reflect.String && kind != reflect.Float64 {
5✔
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++ {
5✔
122
                elem = rVal.Index(i)
2✔
123
                elemKind := elem.Kind()
2✔
124
                if elemKind == reflect.Interface {
4✔
125
                        elemKind = elem.Elem().Kind()
2✔
126
                }
2✔
127
                if elemKind != kind {
3✔
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
2✔
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" 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 {
216✔
167
        if err := bson.Unmarshal(b, (*internalDevice)(d)); err != nil {
216✔
168
                return err
×
169
        }
×
170
        for _, attr := range d.Attributes {
3,512✔
171
                if attr.Scope == AttrScopeSystem {
3,561✔
172
                        switch attr.Name {
265✔
173
                        case AttrNameGroup:
51✔
174
                                group := attr.Value.(string)
51✔
175
                                d.Group = GroupName(group)
51✔
UNCOV
176
                        case AttrNameUpdated:
×
NEW
177
                                if attr.Value != nil {
×
NEW
178
                                        dateTime := attr.Value.(primitive.DateTime)
×
NEW
179
                                        d.UpdatedTs = dateTime.Time()
×
NEW
180
                                }
×
181
                        case AttrNameCreated:
214✔
182
                                dateTime := attr.Value.(primitive.DateTime)
214✔
183
                                d.CreatedTs = dateTime.Time()
214✔
184
                        }
185
                }
186
        }
187
        return nil
216✔
188
}
189

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

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

212
func (did DeviceID) String() string {
168✔
213
        return string(did)
168✔
214
}
168✔
215

216
func (gn GroupName) String() string {
85✔
217
        return string(gn)
85✔
218
}
85✔
219

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

238
// wrapper for device attributes names and values
239
type DeviceAttributes []DeviceAttribute
240

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

252
        return nil
90✔
253
}
254

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

264
func (d DeviceAttributes) Validate() error {
176✔
265
        for _, a := range d {
2,318✔
266
                if err := a.Validate(); err != nil {
2,147✔
267
                        return err
5✔
268
                }
5✔
269
        }
270
        return nil
171✔
271
}
272

273
func GetDeviceAttributeNameReplacer() *strings.Replacer {
3,357✔
274
        return strings.NewReplacer(".", string(runeDot), "$", string(runeDollar))
3,357✔
275
}
3,357✔
276

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

293
        return nil
216✔
294
}
295

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

316
type DeviceUpdate struct {
317
        Id       DeviceID `json:"id"`
318
        Revision uint     `json:"revision"`
319
}
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