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

mendersoftware / inventory / 1565407895

29 Nov 2024 07:58AM UTC coverage: 91.502%. Remained the same
1565407895

push

gitlab-ci

web-flow
Merge pull request #464 from alfrunes/4.4.x

chore(deps): Upgrade docker image to golang:1.23.3-alpine3.20

3144 of 3436 relevant lines covered (91.5%)

151.63 hits per line

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

92.81
/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,726✔
171
                if attr.Scope == AttrScopeSystem {
3,989✔
172
                        switch attr.Name {
479✔
173
                        case AttrNameGroup:
51✔
174
                                group := attr.Value.(string)
51✔
175
                                d.Group = GroupName(group)
51✔
176
                        case AttrNameUpdated:
214✔
177
                                dateTime := attr.Value.(primitive.DateTime)
214✔
178
                                d.UpdatedTs = dateTime.Time()
214✔
179
                        case AttrNameCreated:
214✔
180
                                dateTime := attr.Value.(primitive.DateTime)
214✔
181
                                d.CreatedTs = dateTime.Time()
214✔
182
                        }
183
                }
184
        }
185
        return nil
216✔
186
}
187

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

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

210
func (did DeviceID) String() string {
168✔
211
        return string(did)
168✔
212
}
168✔
213

214
func (gn GroupName) String() string {
85✔
215
        return string(gn)
85✔
216
}
85✔
217

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

236
// wrapper for device attributes names and values
237
type DeviceAttributes []DeviceAttribute
238

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

250
        return nil
90✔
251
}
252

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

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

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

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

291
        return nil
216✔
292
}
293

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

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