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

mendersoftware / inventory / 955593424

pending completion
955593424

Pull #401

gitlab-ci

merlin-northern
fix: attributes udpate: be mindful of the subset of attributes being patched.

Changelog: Title
Ticket: MEN-6643
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #401: fix: attributes udpate: be mindful of the subset of attributes being …

15 of 38 new or added lines in 3 files covered. (39.47%)

21 existing lines in 2 files now uncovered.

3200 of 3562 relevant lines covered (89.84%)

135.41 hits per line

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

94.53
/api/http/api_inventory.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 http
16

17
import (
18
        "context"
19
        "net/http"
20
        "strconv"
21
        "strings"
22
        "time"
23

24
        "github.com/ant0ine/go-json-rest/rest"
25
        validation "github.com/go-ozzo/ozzo-validation/v4"
26
        "github.com/pkg/errors"
27

28
        midentity "github.com/mendersoftware/go-lib-micro/identity"
29
        "github.com/mendersoftware/go-lib-micro/log"
30
        "github.com/mendersoftware/go-lib-micro/rest_utils"
31
        u "github.com/mendersoftware/go-lib-micro/rest_utils"
32

33
        inventory "github.com/mendersoftware/inventory/inv"
34
        "github.com/mendersoftware/inventory/model"
35
        "github.com/mendersoftware/inventory/store"
36
        "github.com/mendersoftware/inventory/utils"
37
        "github.com/mendersoftware/inventory/utils/identity"
38
)
39

40
const (
41
        uriDevices       = "/api/0.1.0/devices"
42
        uriDevice        = "/api/0.1.0/devices/#id"
43
        uriDeviceTags    = "/api/0.1.0/devices/#id/tags"
44
        uriDeviceGroups  = "/api/0.1.0/devices/#id/group"
45
        uriDeviceGroup   = "/api/0.1.0/devices/#id/group/#name"
46
        uriAttributes    = "/api/0.1.0/attributes"
47
        uriGroups        = "/api/0.1.0/groups"
48
        uriGroupsName    = "/api/0.1.0/groups/#name"
49
        uriGroupsDevices = "/api/0.1.0/groups/#name/devices"
50

51
        apiUrlInternalV1         = "/api/internal/v1/inventory"
52
        uriInternalAlive         = apiUrlInternalV1 + "/alive"
53
        uriInternalHealth        = apiUrlInternalV1 + "/health"
54
        uriInternalTenants       = apiUrlInternalV1 + "/tenants"
55
        uriInternalDevices       = apiUrlInternalV1 + "/tenants/#tenant_id/devices"
56
        urlInternalDevicesStatus = apiUrlInternalV1 + "/tenants/#tenant_id/devices/status/#status"
57
        uriInternalDeviceDetails = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id"
58
        uriInternalDeviceGroups  = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id/groups"
59
        urlInternalAttributes    = apiUrlInternalV1 +
60
                "/tenants/#tenant_id/device/#device_id/attribute/scope/#scope"
61
        urlInternalReindex   = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id/reindex"
62
        apiUrlManagementV2   = "/api/management/v2/inventory"
63
        urlFiltersAttributes = apiUrlManagementV2 + "/filters/attributes"
64
        urlFiltersSearch     = apiUrlManagementV2 + "/filters/search"
65

66
        apiUrlInternalV2         = "/api/internal/v2/inventory"
67
        urlInternalFiltersSearch = apiUrlInternalV2 + "/tenants/#tenant_id/filters/search"
68

69
        hdrTotalCount = "X-Total-Count"
70
)
71

72
const (
73
        queryParamGroup          = "group"
74
        queryParamSort           = "sort"
75
        queryParamHasGroup       = "has_group"
76
        queryParamValueSeparator = ":"
77
        queryParamScopeSeparator = "/"
78
        sortOrderAsc             = "asc"
79
        sortOrderDesc            = "desc"
80
        sortAttributeNameIdx     = 0
81
        sortOrderIdx             = 1
82
)
83

84
const (
85
        DefaultTimeout = time.Second * 10
86
)
87

88
// model of device's group name response at /devices/:id/group endpoint
89
type InventoryApiGroup struct {
90
        Group model.GroupName `json:"group"`
91
}
92

93
func (g InventoryApiGroup) Validate() error {
56✔
94
        return g.Group.Validate()
56✔
95
}
56✔
96

97
type inventoryHandlers struct {
98
        inventory inventory.InventoryApp
99
}
100

101
// return an ApiHandler for device admission app
102
func NewInventoryApiHandlers(i inventory.InventoryApp) ApiHandler {
139✔
103
        return &inventoryHandlers{
139✔
104
                inventory: i,
139✔
105
        }
139✔
106
}
139✔
107

108
func (i *inventoryHandlers) GetApp() (rest.App, error) {
139✔
109
        routes := []*rest.Route{
139✔
110
                rest.Get(uriInternalAlive, i.LivelinessHandler),
139✔
111
                rest.Get(uriInternalHealth, i.HealthCheckHandler),
139✔
112

139✔
113
                rest.Get(uriDevices, i.GetDevicesHandler),
139✔
114
                rest.Get(uriDevice, i.GetDeviceHandler),
139✔
115
                rest.Delete(uriDevice, i.DeleteDeviceInventoryHandler),
139✔
116
                rest.Delete(uriDeviceGroup, i.DeleteDeviceGroupHandler),
139✔
117
                rest.Delete(uriGroupsName, i.DeleteGroupHandler),
139✔
118
                rest.Delete(uriGroupsDevices, i.ClearDevicesGroupHandler),
139✔
119
                rest.Patch(uriAttributes, i.UpdateDeviceAttributesHandler),
139✔
120
                rest.Put(uriAttributes, i.UpdateDeviceAttributesHandler),
139✔
121
                rest.Patch(urlInternalAttributes, i.PatchDeviceAttributesInternalHandler),
139✔
122
                rest.Post(urlInternalReindex, i.ReindexDeviceDataHandler),
139✔
123
                rest.Put(uriDeviceGroups, i.AddDeviceToGroupHandler),
139✔
124
                rest.Patch(uriGroupsDevices, i.AppendDevicesToGroup),
139✔
125
                rest.Put(uriDeviceTags, i.UpdateDeviceTagsHandler),
139✔
126
                rest.Patch(uriDeviceTags, i.UpdateDeviceTagsHandler),
139✔
127

139✔
128
                rest.Get(uriDeviceGroups, i.GetDeviceGroupHandler),
139✔
129
                rest.Get(uriGroups, i.GetGroupsHandler),
139✔
130
                rest.Get(uriGroupsDevices, i.GetDevicesByGroupHandler),
139✔
131

139✔
132
                rest.Post(uriInternalTenants, i.CreateTenantHandler),
139✔
133
                rest.Post(uriInternalDevices, i.AddDeviceHandler),
139✔
134
                rest.Delete(uriInternalDeviceDetails, i.DeleteDeviceHandler),
139✔
135
                rest.Post(urlInternalDevicesStatus, i.InternalDevicesStatusHandler),
139✔
136
                rest.Get(uriInternalDeviceGroups, i.GetDeviceGroupsInternalHandler),
139✔
137
                rest.Get(urlFiltersAttributes, i.FiltersAttributesHandler),
139✔
138
                rest.Post(urlFiltersSearch, i.FiltersSearchHandler),
139✔
139

139✔
140
                rest.Post(urlInternalFiltersSearch, i.InternalFiltersSearchHandler),
139✔
141
        }
139✔
142

139✔
143
        app, err := rest.MakeRouter(
139✔
144
                // augment routes with OPTIONS handler
139✔
145
                AutogenOptionsRoutes(routes, AllowHeaderOptionsGenerator)...,
139✔
146
        )
139✔
147
        if err != nil {
139✔
148
                return nil, errors.Wrap(err, "failed to create router")
×
149
        }
×
150

151
        return app, nil
139✔
152

153
}
154

155
func (i *inventoryHandlers) LivelinessHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
156
        w.WriteHeader(http.StatusNoContent)
1✔
157
}
1✔
158

159
func (i *inventoryHandlers) HealthCheckHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
160
        ctx := r.Context()
2✔
161
        l := log.FromContext(ctx)
2✔
162

2✔
163
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
2✔
164
        defer cancel()
2✔
165

2✔
166
        err := i.inventory.HealthCheck(ctx)
2✔
167
        if err != nil {
3✔
168
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
169
                return
1✔
170
        }
1✔
171

172
        w.WriteHeader(http.StatusNoContent)
1✔
173
}
174

175
// `sort` paramater value is an attribute name with optional direction (desc or asc)
176
// separated by colon (:)
177
//
178
// eg. `sort=attr_name1` or `sort=attr_name1:asc`
179
func parseSortParam(r *rest.Request) (*store.Sort, error) {
17✔
180
        sortStr, err := utils.ParseQueryParmStr(r, queryParamSort, false, nil)
17✔
181
        if err != nil {
17✔
182
                return nil, err
×
183
        }
×
184
        if sortStr == "" {
28✔
185
                return nil, nil
11✔
186
        }
11✔
187
        sortValArray := strings.Split(sortStr, queryParamValueSeparator)
6✔
188
        attrNameWithScope := strings.SplitN(
6✔
189
                sortValArray[sortAttributeNameIdx],
6✔
190
                queryParamScopeSeparator,
6✔
191
                2,
6✔
192
        )
6✔
193
        var scope, attrName string
6✔
194
        if len(attrNameWithScope) == 1 {
12✔
195
                scope = model.AttrScopeInventory
6✔
196
                attrName = attrNameWithScope[0]
6✔
197
        } else {
6✔
198
                scope = attrNameWithScope[0]
×
199
                attrName = attrNameWithScope[1]
×
200
        }
×
201
        sort := store.Sort{AttrName: attrName, AttrScope: scope}
6✔
202
        if len(sortValArray) == 2 {
12✔
203
                sortOrder := sortValArray[sortOrderIdx]
6✔
204
                if sortOrder != sortOrderAsc && sortOrder != sortOrderDesc {
7✔
205
                        return nil, errors.New("invalid sort order")
1✔
206
                }
1✔
207
                sort.Ascending = sortOrder == sortOrderAsc
5✔
208
        }
209
        return &sort, nil
5✔
210
}
211

212
// Filter paramaters name are attributes name. Value can be prefixed
213
// with equality operator code (`eq` for =), separated from value by colon (:).
214
// Equality operator default value is `eq`
215
//
216
// eg. `attr_name1=value1` or `attr_name1=eq:value1`
217
func parseFilterParams(r *rest.Request) ([]store.Filter, error) {
25✔
218
        knownParams := []string{
25✔
219
                utils.PageName,
25✔
220
                utils.PerPageName,
25✔
221
                queryParamSort,
25✔
222
                queryParamHasGroup,
25✔
223
                queryParamGroup,
25✔
224
        }
25✔
225
        filters := make([]store.Filter, 0)
25✔
226
        var filter store.Filter
25✔
227
        for name := range r.URL.Query() {
86✔
228
                if utils.ContainsString(name, knownParams) {
110✔
229
                        continue
49✔
230
                }
231
                valueStr, err := utils.ParseQueryParmStr(r, name, false, nil)
12✔
232
                if err != nil {
12✔
233
                        return nil, err
×
234
                }
×
235

236
                attrNameWithScope := strings.SplitN(name, queryParamScopeSeparator, 2)
12✔
237
                var scope, attrName string
12✔
238
                if len(attrNameWithScope) == 1 {
22✔
239
                        scope = model.AttrScopeInventory
10✔
240
                        attrName = attrNameWithScope[0]
10✔
241
                } else {
12✔
242
                        scope = attrNameWithScope[0]
2✔
243
                        attrName = attrNameWithScope[1]
2✔
244
                }
2✔
245
                filter = store.Filter{AttrName: attrName, AttrScope: scope}
12✔
246

12✔
247
                // make sure we parse ':'s in value, it's either:
12✔
248
                // not there
12✔
249
                // after a valid operator specifier
12✔
250
                // or/and inside the value itself(mac, etc), in which case leave it alone
12✔
251
                sepIdx := strings.Index(valueStr, ":")
12✔
252
                if sepIdx == -1 {
16✔
253
                        filter.Value = valueStr
4✔
254
                        filter.Operator = store.Eq
4✔
255
                } else {
12✔
256
                        validOps := []string{"eq"}
8✔
257
                        for _, o := range validOps {
16✔
258
                                if valueStr[:sepIdx] == o {
12✔
259
                                        switch o {
4✔
260
                                        case "eq":
4✔
261
                                                filter.Operator = store.Eq
4✔
262
                                                filter.Value = valueStr[sepIdx+1:]
4✔
263
                                        }
264
                                        break
4✔
265
                                }
266
                        }
267

268
                        if filter.Value == "" {
12✔
269
                                filter.Value = valueStr
4✔
270
                                filter.Operator = store.Eq
4✔
271
                        }
4✔
272
                }
273

274
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
12✔
275
                if err == nil {
15✔
276
                        filter.ValueFloat = &floatValue
3✔
277
                }
3✔
278

279
                timeValue, err := time.Parse("2006-01-02T15:04:05Z", filter.Value)
12✔
280
                if err == nil {
14✔
281
                        filter.ValueTime = &timeValue
2✔
282
                }
2✔
283

284
                filters = append(filters, filter)
12✔
285
        }
286
        return filters, nil
25✔
287
}
288

289
func (i *inventoryHandlers) GetDevicesHandler(w rest.ResponseWriter, r *rest.Request) {
21✔
290
        ctx := r.Context()
21✔
291

21✔
292
        l := log.FromContext(ctx)
21✔
293

21✔
294
        page, perPage, err := utils.ParsePagination(r)
21✔
295
        if err != nil {
24✔
296
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
297
                return
3✔
298
        }
3✔
299

300
        hasGroup, err := utils.ParseQueryParmBool(r, queryParamHasGroup, false, nil)
18✔
301
        if err != nil {
19✔
302
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
303
                return
1✔
304
        }
1✔
305

306
        groupName, err := utils.ParseQueryParmStr(r, "group", false, nil)
17✔
307
        if err != nil {
17✔
308
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
309
                return
×
310
        }
×
311

312
        sort, err := parseSortParam(r)
17✔
313
        if err != nil {
18✔
314
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
315
                return
1✔
316
        }
1✔
317

318
        filters, err := parseFilterParams(r)
16✔
319
        if err != nil {
16✔
320
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
321
                return
×
322
        }
×
323

324
        ld := store.ListQuery{Skip: int((page - 1) * perPage),
16✔
325
                Limit:     int(perPage),
16✔
326
                Filters:   filters,
16✔
327
                Sort:      sort,
16✔
328
                HasGroup:  hasGroup,
16✔
329
                GroupName: groupName}
16✔
330

16✔
331
        devs, totalCount, err := i.inventory.ListDevices(ctx, ld)
16✔
332

16✔
333
        if err != nil {
17✔
334
                u.RestErrWithLogInternal(w, r, l, err)
1✔
335
                return
1✔
336
        }
1✔
337

338
        hasNext := totalCount > int(page*perPage)
15✔
339
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
15✔
340
        for _, l := range links {
38✔
341
                w.Header().Add("Link", l)
23✔
342
        }
23✔
343
        // the response writer will ensure the header name is in Kebab-Pascal-Case
344
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
15✔
345
        _ = w.WriteJson(devs)
15✔
346
}
347

348
func (i *inventoryHandlers) GetDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
15✔
349
        ctx := r.Context()
15✔
350

15✔
351
        l := log.FromContext(ctx)
15✔
352

15✔
353
        deviceID := r.PathParam("id")
15✔
354

15✔
355
        dev, err := i.inventory.GetDevice(ctx, model.DeviceID(deviceID))
15✔
356
        if err != nil {
16✔
357
                u.RestErrWithLogInternal(w, r, l, err)
1✔
358
                return
1✔
359
        }
1✔
360
        if dev == nil {
15✔
361
                u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
362
                return
1✔
363
        }
1✔
364
        if dev.TagsEtag != "" {
22✔
365
                w.Header().Set("ETag", dev.TagsEtag)
9✔
366
        }
9✔
367

368
        _ = w.WriteJson(dev)
13✔
369
}
370

371
func (i *inventoryHandlers) DeleteDeviceInventoryHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
372
        ctx := r.Context()
3✔
373

3✔
374
        l := log.FromContext(ctx)
3✔
375

3✔
376
        deviceID := r.PathParam("id")
3✔
377

3✔
378
        err := i.inventory.ReplaceAttributes(ctx, model.DeviceID(deviceID),
3✔
379
                model.DeviceAttributes{}, model.AttrScopeInventory, "")
3✔
380
        if err != nil && err != store.ErrDevNotFound {
4✔
381
                u.RestErrWithLogInternal(w, r, l, err)
1✔
382
                return
1✔
383
        }
1✔
384

385
        w.WriteHeader(http.StatusNoContent)
2✔
386
}
387

388
func (i *inventoryHandlers) DeleteDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
389
        ctx := r.Context()
3✔
390
        tenantId := r.PathParam("tenant_id")
3✔
391
        if tenantId != "" {
6✔
392
                id := &midentity.Identity{
3✔
393
                        Tenant: tenantId,
3✔
394
                }
3✔
395
                ctx = midentity.WithContext(ctx, id)
3✔
396
        }
3✔
397

398
        l := log.FromContext(ctx)
3✔
399

3✔
400
        deviceID := r.PathParam("device_id")
3✔
401

3✔
402
        err := i.inventory.DeleteDevice(ctx, model.DeviceID(deviceID))
3✔
403
        if err != nil && err != store.ErrDevNotFound {
4✔
404
                u.RestErrWithLogInternal(w, r, l, err)
1✔
405
                return
1✔
406
        }
1✔
407

408
        w.WriteHeader(http.StatusNoContent)
2✔
409
}
410

411
func (i *inventoryHandlers) AddDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
85✔
412
        ctx := r.Context()
85✔
413
        tenantId := r.PathParam("tenant_id")
85✔
414
        if tenantId != "" {
94✔
415
                id := &midentity.Identity{
9✔
416
                        Tenant: tenantId,
9✔
417
                }
9✔
418
                ctx = midentity.WithContext(ctx, id)
9✔
419
        }
9✔
420

421
        l := log.FromContext(ctx)
85✔
422

85✔
423
        dev, err := parseDevice(r)
85✔
424
        if err != nil {
91✔
425
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
6✔
426
                return
6✔
427
        }
6✔
428

429
        err = dev.Attributes.Validate()
79✔
430
        if err != nil {
79✔
431
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
432
                return
×
433
        }
×
434

435
        err = i.inventory.AddDevice(ctx, dev)
79✔
436
        if err != nil {
80✔
437
                u.RestErrWithLogInternal(w, r, l, err)
1✔
438
                return
1✔
439
        }
1✔
440

441
        w.Header().Add("Location", "devices/"+dev.ID.String())
78✔
442
        w.WriteHeader(http.StatusCreated)
78✔
443
}
444

445
func (i *inventoryHandlers) UpdateDeviceAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
13✔
446
        ctx := r.Context()
13✔
447
        l := log.FromContext(ctx)
13✔
448
        //get device ID from JWT token
13✔
449
        idata, err := identity.ExtractIdentityFromHeaders(r.Header)
13✔
450
        if err != nil {
15✔
451
                u.RestErrWithLogMsg(w, r, l, err, http.StatusUnauthorized, "unauthorized")
2✔
452
                return
2✔
453
        }
2✔
454
        deviceID := model.DeviceID(idata.Subject)
11✔
455
        //extract attributes from body
11✔
456
        attrs, err := parseAttributes(r)
11✔
457
        if err != nil {
15✔
458
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
459
                return
4✔
460
        }
4✔
461
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeInventory, "")
7✔
462
}
463

464
func (i *inventoryHandlers) UpdateDeviceTagsHandler(w rest.ResponseWriter, r *rest.Request) {
18✔
465
        ctx := r.Context()
18✔
466
        l := log.FromContext(ctx)
18✔
467

18✔
468
        // get device ID from uri
18✔
469
        deviceID := model.DeviceID(r.PathParam("id"))
18✔
470
        if len(deviceID) < 1 {
18✔
471
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
×
472
                return
×
473
        }
×
474

475
        ifMatchHeader := r.Header.Get("If-Match")
18✔
476

18✔
477
        // extract attributes from body
18✔
478
        attrs, err := parseAttributes(r)
18✔
479
        if err != nil {
18✔
480
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
481
                return
×
482
        }
×
483

484
        // set scope and timestamp for tags attributes
485
        now := time.Now()
18✔
486
        for i := range attrs {
63✔
487
                attrs[i].Scope = model.AttrScopeTags
45✔
488
                if attrs[i].Timestamp == nil {
90✔
489
                        attrs[i].Timestamp = &now
45✔
490
                }
45✔
491
        }
492

493
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeTags, ifMatchHeader)
18✔
494
}
495

496
func (i *inventoryHandlers) updateDeviceAttributes(
497
        w rest.ResponseWriter,
498
        r *rest.Request,
499
        ctx context.Context,
500
        attrs model.DeviceAttributes,
501
        deviceID model.DeviceID,
502
        scope string,
503
        etag string,
504
) {
25✔
505
        l := log.FromContext(ctx)
25✔
506
        var err error
25✔
507

25✔
508
        // upsert or replace the attributes
25✔
509
        if r.Method == http.MethodPatch {
40✔
510
                // we only check if the inventory needs update with PATCH, we assume that calling PUT
15✔
511
                // is a conscious choice of the caller, to write to the db unconditionally.
15✔
512
                if !i.inventory.InventoryNeedsUpdate(
15✔
513
                        ctx,
15✔
514
                        attrs,
15✔
515
                        deviceID,
15✔
516
                        scope,
15✔
517
                ) {
15✔
NEW
518
                        w.WriteHeader(http.StatusNoContent)
×
NEW
519
                        return
×
NEW
520
                }
×
521
                err = i.inventory.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
15✔
522
        } else if r.Method == http.MethodPut {
20✔
523
                err = i.inventory.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
10✔
524
        } else {
10✔
NEW
525
                u.RestErrWithLog(w, r, l, errors.New("method not allowed"), http.StatusMethodNotAllowed)
×
526
                return
×
527
        }
×
528

529
        cause := errors.Cause(err)
25✔
530
        switch cause {
25✔
531
        case store.ErrNoAttrName:
×
532
        case inventory.ErrTooManyAttributes:
1✔
533
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
1✔
534
                return
1✔
535
        case inventory.ErrETagDoesntMatch:
4✔
536
                u.RestErrWithInfoMsg(w, r, l, cause, http.StatusPreconditionFailed, cause.Error())
4✔
537
                return
4✔
538
        }
539
        if err != nil {
21✔
540
                u.RestErrWithLogInternal(w, r, l, err)
1✔
541
                return
1✔
542
        }
1✔
543

544
        w.WriteHeader(http.StatusOK)
19✔
545
}
546

547
func (i *inventoryHandlers) PatchDeviceAttributesInternalHandler(
548
        w rest.ResponseWriter,
549
        r *rest.Request,
550
) {
11✔
551
        ctx := r.Context()
11✔
552
        tenantId := r.PathParam("tenant_id")
11✔
553
        ctx = getTenantContext(ctx, tenantId)
11✔
554

11✔
555
        l := log.FromContext(ctx)
11✔
556

11✔
557
        deviceId := r.PathParam("device_id")
11✔
558
        if len(deviceId) < 1 {
12✔
559
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
560
                return
1✔
561
        }
1✔
562
        //extract attributes from body
563
        attrs, err := parseAttributes(r)
10✔
564
        if err != nil {
14✔
565
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
566
                return
4✔
567
        }
4✔
568
        for _, a := range attrs {
18✔
569
                a.Scope = r.PathParam("scope")
12✔
570
        }
12✔
571

572
        //upsert the attributes
573
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
6✔
574
        cause := errors.Cause(err)
6✔
575
        switch cause {
6✔
576
        case store.ErrNoAttrName:
×
577
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
578
                return
×
579
        }
580
        if err != nil {
7✔
581
                u.RestErrWithLogInternal(w, r, l, err)
1✔
582
                return
1✔
583
        }
1✔
584

585
        w.WriteHeader(http.StatusOK)
5✔
586
}
587

588
func (i *inventoryHandlers) DeleteDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
589
        ctx := r.Context()
7✔
590

7✔
591
        l := log.FromContext(ctx)
7✔
592

7✔
593
        deviceID := r.PathParam("id")
7✔
594
        groupName := r.PathParam("name")
7✔
595

7✔
596
        err := i.inventory.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
7✔
597
        if err != nil {
11✔
598
                cause := errors.Cause(err)
4✔
599
                if cause != nil {
8✔
600
                        if cause.Error() == store.ErrDevNotFound.Error() {
7✔
601
                                u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
3✔
602
                                return
3✔
603
                        }
3✔
604
                }
605
                u.RestErrWithLogInternal(w, r, l, err)
1✔
606
                return
1✔
607
        }
608

609
        w.WriteHeader(http.StatusNoContent)
3✔
610
}
611

612
func (i *inventoryHandlers) AddDeviceToGroupHandler(w rest.ResponseWriter, r *rest.Request) {
57✔
613
        ctx := r.Context()
57✔
614

57✔
615
        l := log.FromContext(ctx)
57✔
616

57✔
617
        devId := r.PathParam("id")
57✔
618

57✔
619
        var group InventoryApiGroup
57✔
620
        err := r.DecodeJsonPayload(&group)
57✔
621
        if err != nil {
58✔
622
                u.RestErrWithLog(
1✔
623
                        w, r, l, errors.Wrap(err, "failed to decode device group data"),
1✔
624
                        http.StatusBadRequest)
1✔
625
                return
1✔
626
        }
1✔
627

628
        if err = group.Validate(); err != nil {
59✔
629
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
630
                return
3✔
631
        }
3✔
632

633
        err = i.inventory.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
53✔
634
        if err != nil {
55✔
635
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
3✔
636
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
1✔
637
                        return
1✔
638
                }
1✔
639
                u.RestErrWithLogInternal(w, r, l, err)
1✔
640
                return
1✔
641
        }
642
        w.WriteHeader(http.StatusNoContent)
51✔
643
}
644

645
func (i *inventoryHandlers) GetDevicesByGroupHandler(w rest.ResponseWriter, r *rest.Request) {
29✔
646
        ctx := r.Context()
29✔
647

29✔
648
        l := log.FromContext(ctx)
29✔
649

29✔
650
        group := r.PathParam("name")
29✔
651

29✔
652
        page, perPage, err := utils.ParsePagination(r)
29✔
653
        if err != nil {
32✔
654
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
655
                return
3✔
656
        }
3✔
657

658
        //get one extra device to see if there's a 'next' page
659
        ids, totalCount, err := i.inventory.ListDevicesByGroup(
26✔
660
                ctx,
26✔
661
                model.GroupName(group),
26✔
662
                int((page-1)*perPage),
26✔
663
                int(perPage),
26✔
664
        )
26✔
665
        if err != nil {
32✔
666
                if err == store.ErrGroupNotFound {
11✔
667
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
5✔
668
                } else {
6✔
669
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
670
                }
1✔
671
                return
6✔
672
        }
673

674
        hasNext := totalCount > int(page*perPage)
20✔
675

20✔
676
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
20✔
677
        for _, l := range links {
43✔
678
                w.Header().Add("Link", l)
23✔
679
        }
23✔
680
        // the response writer will ensure the header name is in Kebab-Pascal-Case
681
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
20✔
682
        _ = w.WriteJson(ids)
20✔
683
}
684

685
func (i *inventoryHandlers) AppendDevicesToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
686
        var deviceIDs []model.DeviceID
5✔
687
        ctx := r.Context()
5✔
688
        l := log.FromContext(ctx)
5✔
689
        groupName := model.GroupName(r.PathParam("name"))
5✔
690
        if err := groupName.Validate(); err != nil {
6✔
691
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
692
                return
1✔
693
        }
1✔
694

695
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
696
                u.RestErrWithLog(w, r, l,
1✔
697
                        errors.Wrap(err, "invalid payload schema"),
1✔
698
                        http.StatusBadRequest,
1✔
699
                )
1✔
700
                return
1✔
701
        } else if len(deviceIDs) == 0 {
5✔
702
                u.RestErrWithLog(w, r, l,
1✔
703
                        errors.New("no device IDs present in payload"),
1✔
704
                        http.StatusBadRequest,
1✔
705
                )
1✔
706
                return
1✔
707
        }
1✔
708
        updated, err := i.inventory.UpdateDevicesGroup(
2✔
709
                ctx, deviceIDs, groupName,
2✔
710
        )
2✔
711
        if err != nil {
3✔
712
                u.RestErrWithLogInternal(w, r, l, err)
1✔
713
                return
1✔
714
        }
1✔
715
        _ = w.WriteJson(updated)
1✔
716
}
717

718
func (i *inventoryHandlers) DeleteGroupHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
719
        ctx := r.Context()
3✔
720
        l := log.FromContext(ctx)
3✔
721

3✔
722
        groupName := model.GroupName(r.PathParam("name"))
3✔
723
        if err := groupName.Validate(); err != nil {
4✔
724
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
725
                return
1✔
726
        }
1✔
727

728
        updated, err := i.inventory.DeleteGroup(ctx, groupName)
2✔
729
        if err != nil {
3✔
730
                u.RestErrWithLogInternal(w, r, l, err)
1✔
731
                return
1✔
732
        }
1✔
733
        w.WriteHeader(http.StatusOK)
1✔
734
        _ = w.WriteJson(updated)
1✔
735
}
736

737
func (i *inventoryHandlers) ClearDevicesGroupHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
738
        var deviceIDs []model.DeviceID
5✔
739
        ctx := r.Context()
5✔
740
        l := log.FromContext(ctx)
5✔
741

5✔
742
        groupName := model.GroupName(r.PathParam("name"))
5✔
743
        if err := groupName.Validate(); err != nil {
6✔
744
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
745
                return
1✔
746
        }
1✔
747

748
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
749
                u.RestErrWithLog(w, r, l,
1✔
750
                        errors.Wrap(err, "invalid payload schema"),
1✔
751
                        http.StatusBadRequest,
1✔
752
                )
1✔
753
                return
1✔
754
        } else if len(deviceIDs) == 0 {
5✔
755
                u.RestErrWithLog(w, r, l,
1✔
756
                        errors.New("no device IDs present in payload"),
1✔
757
                        http.StatusBadRequest,
1✔
758
                )
1✔
759
                return
1✔
760
        }
1✔
761

762
        updated, err := i.inventory.UnsetDevicesGroup(ctx, deviceIDs, groupName)
2✔
763
        if err != nil {
3✔
764
                u.RestErrWithLogInternal(w, r, l, err)
1✔
765
                return
1✔
766
        }
1✔
767
        w.WriteHeader(http.StatusOK)
1✔
768
        _ = w.WriteJson(updated)
1✔
769
}
770

771
func parseDevice(r *rest.Request) (*model.Device, error) {
85✔
772
        dev := model.Device{}
85✔
773

85✔
774
        //decode body
85✔
775
        err := r.DecodeJsonPayload(&dev)
85✔
776
        if err != nil {
88✔
777
                return nil, errors.Wrap(err, "failed to decode request body")
3✔
778
        }
3✔
779

780
        if err := dev.Validate(); err != nil {
85✔
781
                return nil, err
3✔
782
        }
3✔
783

784
        return &dev, nil
79✔
785
}
786

787
func parseAttributes(r *rest.Request) (model.DeviceAttributes, error) {
39✔
788
        var attrs model.DeviceAttributes
39✔
789

39✔
790
        err := r.DecodeJsonPayload(&attrs)
39✔
791
        if err != nil {
43✔
792
                return nil, errors.Wrap(err, "failed to decode request body")
4✔
793
        }
4✔
794

795
        err = attrs.Validate()
35✔
796
        if err != nil {
39✔
797
                return nil, err
4✔
798
        }
4✔
799

800
        return attrs, nil
31✔
801
}
802

803
func (i *inventoryHandlers) GetGroupsHandler(w rest.ResponseWriter, r *rest.Request) {
9✔
804
        var fltr []model.FilterPredicate
9✔
805
        ctx := r.Context()
9✔
806

9✔
807
        l := log.FromContext(ctx)
9✔
808

9✔
809
        query := r.URL.Query()
9✔
810
        status := query.Get("status")
9✔
811
        if status != "" {
10✔
812
                fltr = []model.FilterPredicate{{
1✔
813
                        Attribute: "status",
1✔
814
                        Scope:     "identity",
1✔
815
                        Type:      "$eq",
1✔
816
                        Value:     status,
1✔
817
                }}
1✔
818
        }
1✔
819

820
        groups, err := i.inventory.ListGroups(ctx, fltr)
9✔
821
        if err != nil {
10✔
822
                u.RestErrWithLogInternal(w, r, l, err)
1✔
823
                return
1✔
824
        }
1✔
825

826
        if groups == nil {
9✔
827
                groups = []model.GroupName{}
1✔
828
        }
1✔
829

830
        _ = w.WriteJson(groups)
8✔
831
}
832

833
func (i *inventoryHandlers) GetDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
834
        ctx := r.Context()
4✔
835

4✔
836
        l := log.FromContext(ctx)
4✔
837

4✔
838
        deviceID := r.PathParam("id")
4✔
839

4✔
840
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
841
        if err != nil {
6✔
842
                if err == store.ErrDevNotFound {
3✔
843
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
844
                } else {
2✔
845
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
846
                }
1✔
847
                return
2✔
848
        }
849

850
        ret := map[string]*model.GroupName{"group": nil}
2✔
851

2✔
852
        if group != "" {
3✔
853
                ret["group"] = &group
1✔
854
        }
1✔
855

856
        _ = w.WriteJson(ret)
2✔
857
}
858

859
type newTenantRequest struct {
860
        TenantID string `json:"tenant_id" valid:"required"`
861
}
862

863
func (t newTenantRequest) Validate() error {
7✔
864
        return validation.ValidateStruct(&t,
7✔
865
                validation.Field(&t.TenantID, validation.Required),
7✔
866
        )
7✔
867
}
7✔
868

869
func (i *inventoryHandlers) CreateTenantHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
870
        ctx := r.Context()
8✔
871

8✔
872
        l := log.FromContext(ctx)
8✔
873

8✔
874
        var newTenant newTenantRequest
8✔
875

8✔
876
        if err := r.DecodeJsonPayload(&newTenant); err != nil {
9✔
877
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
878
                return
1✔
879
        }
1✔
880

881
        if err := newTenant.Validate(); err != nil {
9✔
882
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
883
                return
2✔
884
        }
2✔
885

886
        err := i.inventory.CreateTenant(ctx, model.NewTenant{
5✔
887
                ID: newTenant.TenantID,
5✔
888
        })
5✔
889
        if err != nil {
6✔
890
                u.RestErrWithLogInternal(w, r, l, err)
1✔
891
                return
1✔
892
        }
1✔
893

894
        w.WriteHeader(http.StatusCreated)
4✔
895
}
896

897
func (i *inventoryHandlers) FiltersAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
898
        ctx := r.Context()
5✔
899

5✔
900
        l := log.FromContext(ctx)
5✔
901

5✔
902
        // query the database
5✔
903
        attributes, err := i.inventory.GetFiltersAttributes(ctx)
5✔
904
        if err != nil {
6✔
905
                u.RestErrWithLogInternal(w, r, l, err)
1✔
906
                return
1✔
907
        }
1✔
908

909
        // in case of nil make sure we return empty list
910
        if attributes == nil {
6✔
911
                attributes = []model.FilterAttribute{}
2✔
912
        }
2✔
913

914
        _ = w.WriteJson(attributes)
4✔
915
}
916

917
func (i *inventoryHandlers) FiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
918
        ctx := r.Context()
8✔
919

8✔
920
        l := log.FromContext(ctx)
8✔
921

8✔
922
        //extract attributes from body
8✔
923
        searchParams, err := parseSearchParams(r)
8✔
924
        if err != nil {
10✔
925
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
926
                return
2✔
927
        }
2✔
928

929
        // query the database
930
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
6✔
931
        if err != nil {
8✔
932
                if strings.Contains(err.Error(), "BadValue") {
3✔
933
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
934
                } else {
2✔
935
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
936
                }
1✔
937
                return
2✔
938
        }
939

940
        // the response writer will ensure the header name is in Kebab-Pascal-Case
941
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
4✔
942
        _ = w.WriteJson(devs)
4✔
943
}
944

945
func (i *inventoryHandlers) InternalFiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
946
        ctx := r.Context()
5✔
947

5✔
948
        l := log.FromContext(ctx)
5✔
949

5✔
950
        tenantId := r.PathParam("tenant_id")
5✔
951
        if tenantId != "" {
9✔
952
                ctx = getTenantContext(ctx, tenantId)
4✔
953
        }
4✔
954

955
        //extract attributes from body
956
        searchParams, err := parseSearchParams(r)
5✔
957
        if err != nil {
6✔
958
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
959
                return
1✔
960
        }
1✔
961

962
        // query the database
963
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
4✔
964
        if err != nil {
6✔
965
                if strings.Contains(err.Error(), "BadValue") {
3✔
966
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
967
                } else {
2✔
968
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
969
                }
1✔
970
                return
2✔
971
        }
972

973
        // the response writer will ensure the header name is in Kebab-Pascal-Case
974
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
975
        _ = w.WriteJson(devs)
2✔
976
}
977

978
func getTenantContext(ctx context.Context, tenantId string) context.Context {
33✔
979
        if ctx == nil {
33✔
980
                ctx = context.Background()
×
981
        }
×
982
        if tenantId == "" {
35✔
983
                return ctx
2✔
984
        }
2✔
985
        id := &midentity.Identity{
31✔
986
                Tenant: tenantId,
31✔
987
        }
31✔
988

31✔
989
        ctx = midentity.WithContext(ctx, id)
31✔
990

31✔
991
        return ctx
31✔
992
}
993

994
func (i *inventoryHandlers) InternalDevicesStatusHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
995
        const (
8✔
996
                StatusDecommissioned = "decommissioned"
8✔
997
                StatusAccepted       = "accepted"
8✔
998
                StatusRejected       = "rejected"
8✔
999
                StatusPreauthorized  = "preauthorized"
8✔
1000
                StatusPending        = "pending"
8✔
1001
                StatusNoAuth         = "noauth"
8✔
1002
        )
8✔
1003
        var (
8✔
1004
                devices []model.DeviceUpdate
8✔
1005
                result  *model.UpdateResult
8✔
1006
        )
8✔
1007

8✔
1008
        ctx := r.Context()
8✔
1009
        l := log.FromContext(ctx)
8✔
1010

8✔
1011
        tenantID := r.PathParam("tenant_id")
8✔
1012
        ctx = getTenantContext(ctx, tenantID)
8✔
1013

8✔
1014
        status := r.PathParam("status")
8✔
1015

8✔
1016
        err := r.DecodeJsonPayload(&devices)
8✔
1017
        if err != nil {
10✔
1018
                u.RestErrWithLog(w, r, l, errors.Wrap(err, "cant parse devices"), http.StatusBadRequest)
2✔
1019
                return
2✔
1020
        }
2✔
1021

1022
        switch status {
6✔
1023
        case StatusAccepted, StatusPreauthorized,
1024
                StatusPending, StatusRejected,
1025
                StatusNoAuth:
5✔
1026
                // Update statuses
5✔
1027
                attrs := model.DeviceAttributes{{
5✔
1028
                        Name:  "status",
5✔
1029
                        Scope: model.AttrScopeIdentity,
5✔
1030
                        Value: status,
5✔
1031
                }}
5✔
1032
                result, err = i.inventory.UpsertDevicesStatuses(ctx, devices, attrs)
5✔
1033
        case StatusDecommissioned:
×
1034
                // Delete Inventory
×
1035
                result, err = i.inventory.DeleteDevices(ctx, getIdsFromDevices(devices))
×
1036
        default:
1✔
1037
                // Unrecognized status
1✔
1038
                u.RestErrWithLog(w, r, l,
1✔
1039
                        errors.Errorf("unrecognized status: %s", status),
1✔
1040
                        http.StatusNotFound,
1✔
1041
                )
1✔
1042
                return
1✔
1043
        }
1044
        if err == store.ErrWriteConflict {
6✔
1045
                u.RestErrWithLog(w, r, l, err, http.StatusConflict)
1✔
1046
                return
1✔
1047
        } else if err != nil {
6✔
1048
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1049
                return
1✔
1050
        }
1✔
1051

1052
        w.WriteHeader(http.StatusOK)
3✔
1053
        _ = w.WriteJson(result)
3✔
1054
}
1055

1056
func (i *inventoryHandlers) GetDeviceGroupsInternalHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
1057
        ctx := r.Context()
4✔
1058

4✔
1059
        l := log.FromContext(ctx)
4✔
1060

4✔
1061
        tenantId := r.PathParam("tenant_id")
4✔
1062
        ctx = getTenantContext(ctx, tenantId)
4✔
1063

4✔
1064
        deviceID := r.PathParam("device_id")
4✔
1065
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
1066
        if err != nil {
6✔
1067
                if err == store.ErrDevNotFound {
3✔
1068
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
1069
                } else {
2✔
1070
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1071
                }
1✔
1072
                return
2✔
1073
        }
1074

1075
        res := model.DeviceGroups{}
2✔
1076
        if group != "" {
3✔
1077
                res.Groups = append(res.Groups, string(group))
1✔
1078
        }
1✔
1079

1080
        _ = w.WriteJson(res)
2✔
1081
}
1082

1083
func (i *inventoryHandlers) ReindexDeviceDataHandler(w rest.ResponseWriter, r *rest.Request) {
6✔
1084
        ctx := r.Context()
6✔
1085
        tenantId := r.PathParam("tenant_id")
6✔
1086
        ctx = getTenantContext(ctx, tenantId)
6✔
1087

6✔
1088
        l := log.FromContext(ctx)
6✔
1089

6✔
1090
        deviceId := r.PathParam("device_id")
6✔
1091
        if len(deviceId) < 1 {
7✔
1092
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
1093
                return
1✔
1094
        }
1✔
1095

1096
        serviceName, err := utils.ParseQueryParmStr(r, "service", false, nil)
5✔
1097
        // inventory service accepts only reindex requests from devicemonitor
5✔
1098
        if err != nil || serviceName != "devicemonitor" {
6✔
1099
                u.RestErrWithLog(w, r, l, errors.New("unsupported service"), http.StatusBadRequest)
1✔
1100
                return
1✔
1101
        }
1✔
1102

1103
        // check devicemonitor alerts
1104
        alertsCount, err := i.inventory.CheckAlerts(ctx, deviceId)
4✔
1105
        if err != nil {
5✔
1106
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1107
                return
1✔
1108
        }
1✔
1109

1110
        alertsPresent := false
3✔
1111
        if alertsCount > 0 {
4✔
1112
                alertsPresent = true
1✔
1113
        }
1✔
1114
        attrs := model.DeviceAttributes{
3✔
1115
                model.DeviceAttribute{
3✔
1116
                        Name:  model.AttrNameNumberOfAlerts,
3✔
1117
                        Scope: model.AttrScopeMonitor,
3✔
1118
                        Value: alertsCount,
3✔
1119
                },
3✔
1120
                model.DeviceAttribute{
3✔
1121
                        Name:  model.AttrNameAlerts,
3✔
1122
                        Scope: model.AttrScopeMonitor,
3✔
1123
                        Value: alertsPresent,
3✔
1124
                },
3✔
1125
        }
3✔
1126

3✔
1127
        // upsert monitor attributes
3✔
1128
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
3✔
1129
        cause := errors.Cause(err)
3✔
1130
        switch cause {
3✔
1131
        case store.ErrNoAttrName:
×
1132
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
1133
                return
×
1134
        }
1135
        if err != nil {
4✔
1136
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1137
                return
1✔
1138
        }
1✔
1139

1140
        w.WriteHeader(http.StatusOK)
2✔
1141
}
1142

1143
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
×
1144
        ids := make([]model.DeviceID, len(devices))
×
1145
        for i, dev := range devices {
×
1146
                ids[i] = dev.Id
×
1147
        }
×
1148
        return ids
×
1149
}
1150

1151
func parseSearchParams(r *rest.Request) (*model.SearchParams, error) {
19✔
1152
        var searchParams model.SearchParams
19✔
1153

19✔
1154
        if err := r.DecodeJsonPayload(&searchParams); err != nil {
20✔
1155
                return nil, errors.Wrap(err, "failed to decode request body")
1✔
1156
        }
1✔
1157

1158
        if searchParams.Page < 1 {
19✔
1159
                searchParams.Page = utils.PageDefault
1✔
1160
        }
1✔
1161
        if searchParams.PerPage < 1 {
19✔
1162
                searchParams.PerPage = utils.PerPageDefault
1✔
1163
        }
1✔
1164

1165
        if err := searchParams.Validate(); err != nil {
23✔
1166
                return nil, err
5✔
1167
        }
5✔
1168

1169
        return &searchParams, nil
13✔
1170
}
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