• 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

94.8
/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
                err = i.inventory.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
15✔
511
        } else if r.Method == http.MethodPut {
35✔
512
                err = i.inventory.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
10✔
513
        } else {
10✔
514
                u.RestErrWithLog(w, r, l, errors.New("method not alllowed"), http.StatusMethodNotAllowed)
×
515
                return
×
516
        }
×
517

518
        cause := errors.Cause(err)
25✔
519
        switch cause {
25✔
520
        case store.ErrNoAttrName:
×
521
        case inventory.ErrTooManyAttributes:
1✔
522
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
1✔
523
                return
1✔
524
        case inventory.ErrETagDoesntMatch:
4✔
525
                u.RestErrWithInfoMsg(w, r, l, cause, http.StatusPreconditionFailed, cause.Error())
4✔
526
                return
4✔
527
        }
528
        if err != nil {
21✔
529
                u.RestErrWithLogInternal(w, r, l, err)
1✔
530
                return
1✔
531
        }
1✔
532

533
        w.WriteHeader(http.StatusOK)
19✔
534
}
535

536
func (i *inventoryHandlers) PatchDeviceAttributesInternalHandler(
537
        w rest.ResponseWriter,
538
        r *rest.Request,
539
) {
11✔
540
        ctx := r.Context()
11✔
541
        tenantId := r.PathParam("tenant_id")
11✔
542
        ctx = getTenantContext(ctx, tenantId)
11✔
543

11✔
544
        l := log.FromContext(ctx)
11✔
545

11✔
546
        deviceId := r.PathParam("device_id")
11✔
547
        if len(deviceId) < 1 {
12✔
548
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
549
                return
1✔
550
        }
1✔
551
        //extract attributes from body
552
        attrs, err := parseAttributes(r)
10✔
553
        if err != nil {
14✔
554
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
555
                return
4✔
556
        }
4✔
557
        for _, a := range attrs {
18✔
558
                a.Scope = r.PathParam("scope")
12✔
559
        }
12✔
560

561
        //upsert the attributes
562
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
6✔
563
        cause := errors.Cause(err)
6✔
564
        switch cause {
6✔
565
        case store.ErrNoAttrName:
×
566
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
567
                return
×
568
        }
569
        if err != nil {
7✔
570
                u.RestErrWithLogInternal(w, r, l, err)
1✔
571
                return
1✔
572
        }
1✔
573

574
        w.WriteHeader(http.StatusOK)
5✔
575
}
576

577
func (i *inventoryHandlers) DeleteDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
578
        ctx := r.Context()
7✔
579

7✔
580
        l := log.FromContext(ctx)
7✔
581

7✔
582
        deviceID := r.PathParam("id")
7✔
583
        groupName := r.PathParam("name")
7✔
584

7✔
585
        err := i.inventory.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
7✔
586
        if err != nil {
11✔
587
                cause := errors.Cause(err)
4✔
588
                if cause != nil {
8✔
589
                        if cause.Error() == store.ErrDevNotFound.Error() {
7✔
590
                                u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
3✔
591
                                return
3✔
592
                        }
3✔
593
                }
594
                u.RestErrWithLogInternal(w, r, l, err)
1✔
595
                return
1✔
596
        }
597

598
        w.WriteHeader(http.StatusNoContent)
3✔
599
}
600

601
func (i *inventoryHandlers) AddDeviceToGroupHandler(w rest.ResponseWriter, r *rest.Request) {
57✔
602
        ctx := r.Context()
57✔
603

57✔
604
        l := log.FromContext(ctx)
57✔
605

57✔
606
        devId := r.PathParam("id")
57✔
607

57✔
608
        var group InventoryApiGroup
57✔
609
        err := r.DecodeJsonPayload(&group)
57✔
610
        if err != nil {
58✔
611
                u.RestErrWithLog(
1✔
612
                        w, r, l, errors.Wrap(err, "failed to decode device group data"),
1✔
613
                        http.StatusBadRequest)
1✔
614
                return
1✔
615
        }
1✔
616

617
        if err = group.Validate(); err != nil {
59✔
618
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
619
                return
3✔
620
        }
3✔
621

622
        err = i.inventory.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
53✔
623
        if err != nil {
55✔
624
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
3✔
625
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
1✔
626
                        return
1✔
627
                }
1✔
628
                u.RestErrWithLogInternal(w, r, l, err)
1✔
629
                return
1✔
630
        }
631
        w.WriteHeader(http.StatusNoContent)
51✔
632
}
633

634
func (i *inventoryHandlers) GetDevicesByGroupHandler(w rest.ResponseWriter, r *rest.Request) {
29✔
635
        ctx := r.Context()
29✔
636

29✔
637
        l := log.FromContext(ctx)
29✔
638

29✔
639
        group := r.PathParam("name")
29✔
640

29✔
641
        page, perPage, err := utils.ParsePagination(r)
29✔
642
        if err != nil {
32✔
643
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
644
                return
3✔
645
        }
3✔
646

647
        //get one extra device to see if there's a 'next' page
648
        ids, totalCount, err := i.inventory.ListDevicesByGroup(
26✔
649
                ctx,
26✔
650
                model.GroupName(group),
26✔
651
                int((page-1)*perPage),
26✔
652
                int(perPage),
26✔
653
        )
26✔
654
        if err != nil {
32✔
655
                if err == store.ErrGroupNotFound {
11✔
656
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
5✔
657
                } else {
6✔
658
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
659
                }
1✔
660
                return
6✔
661
        }
662

663
        hasNext := totalCount > int(page*perPage)
20✔
664

20✔
665
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
20✔
666
        for _, l := range links {
43✔
667
                w.Header().Add("Link", l)
23✔
668
        }
23✔
669
        // the response writer will ensure the header name is in Kebab-Pascal-Case
670
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
20✔
671
        _ = w.WriteJson(ids)
20✔
672
}
673

674
func (i *inventoryHandlers) AppendDevicesToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
675
        var deviceIDs []model.DeviceID
5✔
676
        ctx := r.Context()
5✔
677
        l := log.FromContext(ctx)
5✔
678
        groupName := model.GroupName(r.PathParam("name"))
5✔
679
        if err := groupName.Validate(); err != nil {
6✔
680
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
681
                return
1✔
682
        }
1✔
683

684
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
685
                u.RestErrWithLog(w, r, l,
1✔
686
                        errors.Wrap(err, "invalid payload schema"),
1✔
687
                        http.StatusBadRequest,
1✔
688
                )
1✔
689
                return
1✔
690
        } else if len(deviceIDs) == 0 {
5✔
691
                u.RestErrWithLog(w, r, l,
1✔
692
                        errors.New("no device IDs present in payload"),
1✔
693
                        http.StatusBadRequest,
1✔
694
                )
1✔
695
                return
1✔
696
        }
1✔
697
        updated, err := i.inventory.UpdateDevicesGroup(
2✔
698
                ctx, deviceIDs, groupName,
2✔
699
        )
2✔
700
        if err != nil {
3✔
701
                u.RestErrWithLogInternal(w, r, l, err)
1✔
702
                return
1✔
703
        }
1✔
704
        _ = w.WriteJson(updated)
1✔
705
}
706

707
func (i *inventoryHandlers) DeleteGroupHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
708
        ctx := r.Context()
3✔
709
        l := log.FromContext(ctx)
3✔
710

3✔
711
        groupName := model.GroupName(r.PathParam("name"))
3✔
712
        if err := groupName.Validate(); err != nil {
4✔
713
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
714
                return
1✔
715
        }
1✔
716

717
        updated, err := i.inventory.DeleteGroup(ctx, groupName)
2✔
718
        if err != nil {
3✔
719
                u.RestErrWithLogInternal(w, r, l, err)
1✔
720
                return
1✔
721
        }
1✔
722
        w.WriteHeader(http.StatusOK)
1✔
723
        _ = w.WriteJson(updated)
1✔
724
}
725

726
func (i *inventoryHandlers) ClearDevicesGroupHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
727
        var deviceIDs []model.DeviceID
5✔
728
        ctx := r.Context()
5✔
729
        l := log.FromContext(ctx)
5✔
730

5✔
731
        groupName := model.GroupName(r.PathParam("name"))
5✔
732
        if err := groupName.Validate(); err != nil {
6✔
733
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
734
                return
1✔
735
        }
1✔
736

737
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
738
                u.RestErrWithLog(w, r, l,
1✔
739
                        errors.Wrap(err, "invalid payload schema"),
1✔
740
                        http.StatusBadRequest,
1✔
741
                )
1✔
742
                return
1✔
743
        } else if len(deviceIDs) == 0 {
5✔
744
                u.RestErrWithLog(w, r, l,
1✔
745
                        errors.New("no device IDs present in payload"),
1✔
746
                        http.StatusBadRequest,
1✔
747
                )
1✔
748
                return
1✔
749
        }
1✔
750

751
        updated, err := i.inventory.UnsetDevicesGroup(ctx, deviceIDs, groupName)
2✔
752
        if err != nil {
3✔
753
                u.RestErrWithLogInternal(w, r, l, err)
1✔
754
                return
1✔
755
        }
1✔
756
        w.WriteHeader(http.StatusOK)
1✔
757
        _ = w.WriteJson(updated)
1✔
758
}
759

760
func parseDevice(r *rest.Request) (*model.Device, error) {
85✔
761
        dev := model.Device{}
85✔
762

85✔
763
        //decode body
85✔
764
        err := r.DecodeJsonPayload(&dev)
85✔
765
        if err != nil {
88✔
766
                return nil, errors.Wrap(err, "failed to decode request body")
3✔
767
        }
3✔
768

769
        if err := dev.Validate(); err != nil {
85✔
770
                return nil, err
3✔
771
        }
3✔
772

773
        return &dev, nil
79✔
774
}
775

776
func parseAttributes(r *rest.Request) (model.DeviceAttributes, error) {
39✔
777
        var attrs model.DeviceAttributes
39✔
778

39✔
779
        err := r.DecodeJsonPayload(&attrs)
39✔
780
        if err != nil {
43✔
781
                return nil, errors.Wrap(err, "failed to decode request body")
4✔
782
        }
4✔
783

784
        err = attrs.Validate()
35✔
785
        if err != nil {
39✔
786
                return nil, err
4✔
787
        }
4✔
788

789
        return attrs, nil
31✔
790
}
791

792
func (i *inventoryHandlers) GetGroupsHandler(w rest.ResponseWriter, r *rest.Request) {
9✔
793
        var fltr []model.FilterPredicate
9✔
794
        ctx := r.Context()
9✔
795

9✔
796
        l := log.FromContext(ctx)
9✔
797

9✔
798
        query := r.URL.Query()
9✔
799
        status := query.Get("status")
9✔
800
        if status != "" {
10✔
801
                fltr = []model.FilterPredicate{{
1✔
802
                        Attribute: "status",
1✔
803
                        Scope:     "identity",
1✔
804
                        Type:      "$eq",
1✔
805
                        Value:     status,
1✔
806
                }}
1✔
807
        }
1✔
808

809
        groups, err := i.inventory.ListGroups(ctx, fltr)
9✔
810
        if err != nil {
10✔
811
                u.RestErrWithLogInternal(w, r, l, err)
1✔
812
                return
1✔
813
        }
1✔
814

815
        if groups == nil {
9✔
816
                groups = []model.GroupName{}
1✔
817
        }
1✔
818

819
        _ = w.WriteJson(groups)
8✔
820
}
821

822
func (i *inventoryHandlers) GetDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
823
        ctx := r.Context()
4✔
824

4✔
825
        l := log.FromContext(ctx)
4✔
826

4✔
827
        deviceID := r.PathParam("id")
4✔
828

4✔
829
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
830
        if err != nil {
6✔
831
                if err == store.ErrDevNotFound {
3✔
832
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
833
                } else {
2✔
834
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
835
                }
1✔
836
                return
2✔
837
        }
838

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

2✔
841
        if group != "" {
3✔
842
                ret["group"] = &group
1✔
843
        }
1✔
844

845
        _ = w.WriteJson(ret)
2✔
846
}
847

848
type newTenantRequest struct {
849
        TenantID string `json:"tenant_id" valid:"required"`
850
}
851

852
func (t newTenantRequest) Validate() error {
7✔
853
        return validation.ValidateStruct(&t,
7✔
854
                validation.Field(&t.TenantID, validation.Required),
7✔
855
        )
7✔
856
}
7✔
857

858
func (i *inventoryHandlers) CreateTenantHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
859
        ctx := r.Context()
8✔
860

8✔
861
        l := log.FromContext(ctx)
8✔
862

8✔
863
        var newTenant newTenantRequest
8✔
864

8✔
865
        if err := r.DecodeJsonPayload(&newTenant); err != nil {
9✔
866
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
867
                return
1✔
868
        }
1✔
869

870
        if err := newTenant.Validate(); err != nil {
9✔
871
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
872
                return
2✔
873
        }
2✔
874

875
        err := i.inventory.CreateTenant(ctx, model.NewTenant{
5✔
876
                ID: newTenant.TenantID,
5✔
877
        })
5✔
878
        if err != nil {
6✔
879
                u.RestErrWithLogInternal(w, r, l, err)
1✔
880
                return
1✔
881
        }
1✔
882

883
        w.WriteHeader(http.StatusCreated)
4✔
884
}
885

886
func (i *inventoryHandlers) FiltersAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
887
        ctx := r.Context()
5✔
888

5✔
889
        l := log.FromContext(ctx)
5✔
890

5✔
891
        // query the database
5✔
892
        attributes, err := i.inventory.GetFiltersAttributes(ctx)
5✔
893
        if err != nil {
6✔
894
                u.RestErrWithLogInternal(w, r, l, err)
1✔
895
                return
1✔
896
        }
1✔
897

898
        // in case of nil make sure we return empty list
899
        if attributes == nil {
6✔
900
                attributes = []model.FilterAttribute{}
2✔
901
        }
2✔
902

903
        _ = w.WriteJson(attributes)
4✔
904
}
905

906
func (i *inventoryHandlers) FiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
907
        ctx := r.Context()
8✔
908

8✔
909
        l := log.FromContext(ctx)
8✔
910

8✔
911
        //extract attributes from body
8✔
912
        searchParams, err := parseSearchParams(r)
8✔
913
        if err != nil {
10✔
914
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
915
                return
2✔
916
        }
2✔
917

918
        // query the database
919
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
6✔
920
        if err != nil {
8✔
921
                if strings.Contains(err.Error(), "BadValue") {
3✔
922
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
923
                } else {
2✔
924
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
925
                }
1✔
926
                return
2✔
927
        }
928

929
        // the response writer will ensure the header name is in Kebab-Pascal-Case
930
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
4✔
931
        _ = w.WriteJson(devs)
4✔
932
}
933

934
func (i *inventoryHandlers) InternalFiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
935
        ctx := r.Context()
5✔
936

5✔
937
        l := log.FromContext(ctx)
5✔
938

5✔
939
        tenantId := r.PathParam("tenant_id")
5✔
940
        if tenantId != "" {
9✔
941
                ctx = getTenantContext(ctx, tenantId)
4✔
942
        }
4✔
943

944
        //extract attributes from body
945
        searchParams, err := parseSearchParams(r)
5✔
946
        if err != nil {
6✔
947
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
948
                return
1✔
949
        }
1✔
950

951
        // query the database
952
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
4✔
953
        if err != nil {
6✔
954
                if strings.Contains(err.Error(), "BadValue") {
3✔
955
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
956
                } else {
2✔
957
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
958
                }
1✔
959
                return
2✔
960
        }
961

962
        // the response writer will ensure the header name is in Kebab-Pascal-Case
963
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
964
        _ = w.WriteJson(devs)
2✔
965
}
966

967
func getTenantContext(ctx context.Context, tenantId string) context.Context {
33✔
968
        if ctx == nil {
33✔
969
                ctx = context.Background()
×
970
        }
×
971
        if tenantId == "" {
35✔
972
                return ctx
2✔
973
        }
2✔
974
        id := &midentity.Identity{
31✔
975
                Tenant: tenantId,
31✔
976
        }
31✔
977

31✔
978
        ctx = midentity.WithContext(ctx, id)
31✔
979

31✔
980
        return ctx
31✔
981
}
982

983
func (i *inventoryHandlers) InternalDevicesStatusHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
984
        const (
8✔
985
                StatusDecommissioned = "decommissioned"
8✔
986
                StatusAccepted       = "accepted"
8✔
987
                StatusRejected       = "rejected"
8✔
988
                StatusPreauthorized  = "preauthorized"
8✔
989
                StatusPending        = "pending"
8✔
990
                StatusNoAuth         = "noauth"
8✔
991
        )
8✔
992
        var (
8✔
993
                devices []model.DeviceUpdate
8✔
994
                result  *model.UpdateResult
8✔
995
        )
8✔
996

8✔
997
        ctx := r.Context()
8✔
998
        l := log.FromContext(ctx)
8✔
999

8✔
1000
        tenantID := r.PathParam("tenant_id")
8✔
1001
        ctx = getTenantContext(ctx, tenantID)
8✔
1002

8✔
1003
        status := r.PathParam("status")
8✔
1004

8✔
1005
        err := r.DecodeJsonPayload(&devices)
8✔
1006
        if err != nil {
10✔
1007
                u.RestErrWithLog(w, r, l, errors.Wrap(err, "cant parse devices"), http.StatusBadRequest)
2✔
1008
                return
2✔
1009
        }
2✔
1010

1011
        switch status {
6✔
1012
        case StatusAccepted, StatusPreauthorized,
1013
                StatusPending, StatusRejected,
1014
                StatusNoAuth:
5✔
1015
                // Update statuses
5✔
1016
                attrs := model.DeviceAttributes{{
5✔
1017
                        Name:  "status",
5✔
1018
                        Scope: model.AttrScopeIdentity,
5✔
1019
                        Value: status,
5✔
1020
                }}
5✔
1021
                result, err = i.inventory.UpsertDevicesStatuses(ctx, devices, attrs)
5✔
1022
        case StatusDecommissioned:
×
1023
                // Delete Inventory
×
1024
                result, err = i.inventory.DeleteDevices(ctx, getIdsFromDevices(devices))
×
1025
        default:
1✔
1026
                // Unrecognized status
1✔
1027
                u.RestErrWithLog(w, r, l,
1✔
1028
                        errors.Errorf("unrecognized status: %s", status),
1✔
1029
                        http.StatusNotFound,
1✔
1030
                )
1✔
1031
                return
1✔
1032
        }
1033
        if err == store.ErrWriteConflict {
6✔
1034
                u.RestErrWithLog(w, r, l, err, http.StatusConflict)
1✔
1035
                return
1✔
1036
        } else if err != nil {
6✔
1037
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1038
                return
1✔
1039
        }
1✔
1040

1041
        w.WriteHeader(http.StatusOK)
3✔
1042
        _ = w.WriteJson(result)
3✔
1043
}
1044

1045
func (i *inventoryHandlers) GetDeviceGroupsInternalHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
1046
        ctx := r.Context()
4✔
1047

4✔
1048
        l := log.FromContext(ctx)
4✔
1049

4✔
1050
        tenantId := r.PathParam("tenant_id")
4✔
1051
        ctx = getTenantContext(ctx, tenantId)
4✔
1052

4✔
1053
        deviceID := r.PathParam("device_id")
4✔
1054
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
1055
        if err != nil {
6✔
1056
                if err == store.ErrDevNotFound {
3✔
1057
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
1058
                } else {
2✔
1059
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1060
                }
1✔
1061
                return
2✔
1062
        }
1063

1064
        res := model.DeviceGroups{}
2✔
1065
        if group != "" {
3✔
1066
                res.Groups = append(res.Groups, string(group))
1✔
1067
        }
1✔
1068

1069
        _ = w.WriteJson(res)
2✔
1070
}
1071

1072
func (i *inventoryHandlers) ReindexDeviceDataHandler(w rest.ResponseWriter, r *rest.Request) {
6✔
1073
        ctx := r.Context()
6✔
1074
        tenantId := r.PathParam("tenant_id")
6✔
1075
        ctx = getTenantContext(ctx, tenantId)
6✔
1076

6✔
1077
        l := log.FromContext(ctx)
6✔
1078

6✔
1079
        deviceId := r.PathParam("device_id")
6✔
1080
        if len(deviceId) < 1 {
7✔
1081
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
1082
                return
1✔
1083
        }
1✔
1084

1085
        serviceName, err := utils.ParseQueryParmStr(r, "service", false, nil)
5✔
1086
        // inventory service accepts only reindex requests from devicemonitor
5✔
1087
        if err != nil || serviceName != "devicemonitor" {
6✔
1088
                u.RestErrWithLog(w, r, l, errors.New("unsupported service"), http.StatusBadRequest)
1✔
1089
                return
1✔
1090
        }
1✔
1091

1092
        // check devicemonitor alerts
1093
        alertsCount, err := i.inventory.CheckAlerts(ctx, deviceId)
4✔
1094
        if err != nil {
5✔
1095
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1096
                return
1✔
1097
        }
1✔
1098

1099
        alertsPresent := false
3✔
1100
        if alertsCount > 0 {
4✔
1101
                alertsPresent = true
1✔
1102
        }
1✔
1103
        attrs := model.DeviceAttributes{
3✔
1104
                model.DeviceAttribute{
3✔
1105
                        Name:  model.AttrNameNumberOfAlerts,
3✔
1106
                        Scope: model.AttrScopeMonitor,
3✔
1107
                        Value: alertsCount,
3✔
1108
                },
3✔
1109
                model.DeviceAttribute{
3✔
1110
                        Name:  model.AttrNameAlerts,
3✔
1111
                        Scope: model.AttrScopeMonitor,
3✔
1112
                        Value: alertsPresent,
3✔
1113
                },
3✔
1114
        }
3✔
1115

3✔
1116
        // upsert monitor attributes
3✔
1117
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
3✔
1118
        cause := errors.Cause(err)
3✔
1119
        switch cause {
3✔
1120
        case store.ErrNoAttrName:
×
1121
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
1122
                return
×
1123
        }
1124
        if err != nil {
4✔
1125
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1126
                return
1✔
1127
        }
1✔
1128

1129
        w.WriteHeader(http.StatusOK)
2✔
1130
}
1131

1132
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
×
1133
        ids := make([]model.DeviceID, len(devices))
×
1134
        for i, dev := range devices {
×
1135
                ids[i] = dev.Id
×
1136
        }
×
1137
        return ids
×
1138
}
1139

1140
func parseSearchParams(r *rest.Request) (*model.SearchParams, error) {
19✔
1141
        var searchParams model.SearchParams
19✔
1142

19✔
1143
        if err := r.DecodeJsonPayload(&searchParams); err != nil {
20✔
1144
                return nil, errors.Wrap(err, "failed to decode request body")
1✔
1145
        }
1✔
1146

1147
        if searchParams.Page < 1 {
19✔
1148
                searchParams.Page = utils.PageDefault
1✔
1149
        }
1✔
1150
        if searchParams.PerPage < 1 {
19✔
1151
                searchParams.PerPage = utils.PerPageDefault
1✔
1152
        }
1✔
1153

1154
        if err := searchParams.Validate(); err != nil {
23✔
1155
                return nil, err
5✔
1156
        }
5✔
1157

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