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

mendersoftware / inventory / 1285819665

10 May 2024 10:26AM UTC coverage: 91.217% (-0.2%) from 91.379%
1285819665

Pull #455

gitlab-ci

kjaskiewiczz
fix: store "check_in_time" attribute as ISODate instead of string

Changelog: Title
Ticket: MEN-7259

Signed-off-by: Krzysztof Jaskiewicz <krzysztof.jaskiewicz@northern.tech>
Pull Request #455: fix: store "check_in_time" attribute as ISODate instead of string

2 of 8 new or added lines in 1 file covered. (25.0%)

16 existing lines in 1 file now uncovered.

3095 of 3393 relevant lines covered (91.22%)

148.68 hits per line

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

94.35
/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
        "fmt"
20
        "net/http"
21
        "strconv"
22
        "strings"
23
        "time"
24

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

29
        "github.com/mendersoftware/go-lib-micro/accesslog"
30
        "github.com/mendersoftware/go-lib-micro/identity"
31
        midentity "github.com/mendersoftware/go-lib-micro/identity"
32
        "github.com/mendersoftware/go-lib-micro/log"
33
        "github.com/mendersoftware/go-lib-micro/requestid"
34
        "github.com/mendersoftware/go-lib-micro/requestlog"
35
        "github.com/mendersoftware/go-lib-micro/rest_utils"
36
        u "github.com/mendersoftware/go-lib-micro/rest_utils"
37

38
        inventory "github.com/mendersoftware/inventory/inv"
39
        "github.com/mendersoftware/inventory/model"
40
        "github.com/mendersoftware/inventory/store"
41
        "github.com/mendersoftware/inventory/utils"
42
)
43

44
const (
45
        uriDevices       = "/api/0.1.0/devices"
46
        uriDevice        = "/api/0.1.0/devices/#id"
47
        uriDeviceTags    = "/api/0.1.0/devices/#id/tags"
48
        uriDeviceGroups  = "/api/0.1.0/devices/#id/group"
49
        uriDeviceGroup   = "/api/0.1.0/devices/#id/group/#name"
50
        uriAttributes    = "/api/0.1.0/attributes"
51
        uriGroups        = "/api/0.1.0/groups"
52
        uriGroupsName    = "/api/0.1.0/groups/#name"
53
        uriGroupsDevices = "/api/0.1.0/groups/#name/devices"
54

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

70
        apiUrlInternalV2         = "/api/internal/v2/inventory"
71
        urlInternalFiltersSearch = apiUrlInternalV2 + "/tenants/#tenant_id/filters/search"
72

73
        hdrTotalCount = "X-Total-Count"
74
)
75

76
const (
77
        queryParamGroup          = "group"
78
        queryParamSort           = "sort"
79
        queryParamHasGroup       = "has_group"
80
        queryParamValueSeparator = ":"
81
        queryParamScopeSeparator = "/"
82
        sortOrderAsc             = "asc"
83
        sortOrderDesc            = "desc"
84
        sortAttributeNameIdx     = 0
85
        sortOrderIdx             = 1
86
)
87

88
const (
89
        DefaultTimeout = time.Second * 10
90
)
91

92
const (
93
        checkInTimeParamName = "check_in_time"
94
)
95

96
// model of device's group name response at /devices/:id/group endpoint
97
type InventoryApiGroup struct {
98
        Group model.GroupName `json:"group"`
99
}
100

101
func (g InventoryApiGroup) Validate() error {
56✔
102
        return g.Group.Validate()
56✔
103
}
56✔
104

105
type inventoryHandlers struct {
106
        inventory inventory.InventoryApp
107
}
108

109
// return an ApiHandler for device admission app
110
func NewInventoryApiHandlers(i inventory.InventoryApp) ApiHandler {
139✔
111
        return &inventoryHandlers{
139✔
112
                inventory: i,
139✔
113
        }
139✔
114
}
139✔
115

116
func wrapRoutes(middleware rest.Middleware, routes ...*rest.Route) []*rest.Route {
139✔
117
        for _, route := range routes {
4,031✔
118
                route.Func = middleware.MiddlewareFunc(route.Func)
3,892✔
119
        }
3,892✔
120
        return routes
139✔
121
}
122

123
func (i *inventoryHandlers) Build() (http.Handler, error) {
139✔
124
        //this will override the framework's error resp to the desired one:
139✔
125
        // {"error": "msg"}
139✔
126
        // instead of:
139✔
127
        // {"Error": "msg"}
139✔
128
        rest.ErrorFieldName = "error"
139✔
129

139✔
130
        api := rest.NewApi()
139✔
131

139✔
132
        api.Use(
139✔
133
                &requestlog.RequestLogMiddleware{},
139✔
134
                &requestid.RequestIdMiddleware{},
139✔
135
                &accesslog.AccessLogMiddleware{
139✔
136
                        Format: accesslog.SimpleLogFormat,
139✔
137
                        DisableLog: func(statusCode int, r *rest.Request) bool {
474✔
138
                                if r.URL.Path == uriInternalAlive ||
335✔
139
                                        r.URL.Path == uriInternalHealth &&
335✔
140
                                                statusCode < 300 {
337✔
141
                                        // Skips the health/liveliness probes
2✔
142
                                        return true
2✔
143
                                }
2✔
144
                                return false
333✔
145
                        },
146
                },
147
                &rest.ContentTypeCheckerMiddleware{},
148
        )
149
        internalRoutes := []*rest.Route{
139✔
150
                rest.Get(uriInternalAlive, i.LivelinessHandler),
139✔
151
                rest.Get(uriInternalHealth, i.HealthCheckHandler),
139✔
152

139✔
153
                rest.Patch(urlInternalAttributes, i.PatchDeviceAttributesInternalHandler),
139✔
154
                rest.Post(urlInternalReindex, i.ReindexDeviceDataHandler),
139✔
155

139✔
156
                rest.Post(uriInternalTenants, i.CreateTenantHandler),
139✔
157
                rest.Post(uriInternalDevices, i.AddDeviceHandler),
139✔
158
                rest.Delete(uriInternalDeviceDetails, i.DeleteDeviceHandler),
139✔
159
                rest.Post(urlInternalDevicesStatus, i.InternalDevicesStatusHandler),
139✔
160
                rest.Get(uriInternalDeviceGroups, i.GetDeviceGroupsInternalHandler),
139✔
161
                rest.Post(urlInternalFiltersSearch, i.InternalFiltersSearchHandler),
139✔
162
        }
139✔
163

139✔
164
        publicRoutes := AutogenOptionsRoutes([]*rest.Route{
139✔
165
                rest.Get(uriDevices, i.GetDevicesHandler),
139✔
166
                rest.Get(uriDevice, i.GetDeviceHandler),
139✔
167
                rest.Delete(uriDevice, i.DeleteDeviceInventoryHandler),
139✔
168
                rest.Delete(uriDeviceGroup, i.DeleteDeviceGroupHandler),
139✔
169
                rest.Delete(uriGroupsName, i.DeleteGroupHandler),
139✔
170
                rest.Delete(uriGroupsDevices, i.ClearDevicesGroupHandler),
139✔
171
                rest.Patch(uriAttributes, i.UpdateDeviceAttributesHandler),
139✔
172
                rest.Put(uriAttributes, i.UpdateDeviceAttributesHandler),
139✔
173
                rest.Put(uriDeviceGroups, i.AddDeviceToGroupHandler),
139✔
174
                rest.Patch(uriGroupsDevices, i.AppendDevicesToGroup),
139✔
175
                rest.Put(uriDeviceTags, i.UpdateDeviceTagsHandler),
139✔
176
                rest.Patch(uriDeviceTags, i.UpdateDeviceTagsHandler),
139✔
177

139✔
178
                rest.Get(uriDeviceGroups, i.GetDeviceGroupHandler),
139✔
179
                rest.Get(uriGroups, i.GetGroupsHandler),
139✔
180
                rest.Get(uriGroupsDevices, i.GetDevicesByGroupHandler),
139✔
181

139✔
182
                rest.Get(urlFiltersAttributes, i.FiltersAttributesHandler),
139✔
183
                rest.Post(urlFiltersSearch, i.FiltersSearchHandler),
139✔
184
        }, AllowHeaderOptionsGenerator)
139✔
185
        publicRoutes = wrapRoutes(&identity.IdentityMiddleware{
139✔
186
                UpdateLogger: true,
139✔
187
        }, publicRoutes...)
139✔
188
        routes := append(internalRoutes, publicRoutes...)
139✔
189

139✔
190
        app, err := rest.MakeRouter(routes...)
139✔
191
        if err != nil {
139✔
UNCOV
192
                return nil, errors.Wrap(err, "failed to create router")
×
193
        }
×
194
        api.SetApp(app)
139✔
195

139✔
196
        return api.MakeHandler(), nil
139✔
197

198
}
199

200
func (i *inventoryHandlers) LivelinessHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
201
        w.WriteHeader(http.StatusNoContent)
1✔
202
}
1✔
203

204
func (i *inventoryHandlers) HealthCheckHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
205
        ctx := r.Context()
2✔
206
        l := log.FromContext(ctx)
2✔
207

2✔
208
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
2✔
209
        defer cancel()
2✔
210

2✔
211
        err := i.inventory.HealthCheck(ctx)
2✔
212
        if err != nil {
3✔
213
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
214
                return
1✔
215
        }
1✔
216

217
        w.WriteHeader(http.StatusNoContent)
1✔
218
}
219

220
// `sort` paramater value is an attribute name with optional direction (desc or asc)
221
// separated by colon (:)
222
//
223
// eg. `sort=attr_name1` or `sort=attr_name1:asc`
224
func parseSortParam(r *rest.Request) (*store.Sort, error) {
17✔
225
        sortStr, err := utils.ParseQueryParmStr(r, queryParamSort, false, nil)
17✔
226
        if err != nil {
17✔
UNCOV
227
                return nil, err
×
228
        }
×
229
        if sortStr == "" {
28✔
230
                return nil, nil
11✔
231
        }
11✔
232
        sortValArray := strings.Split(sortStr, queryParamValueSeparator)
6✔
233
        attrNameWithScope := strings.SplitN(
6✔
234
                sortValArray[sortAttributeNameIdx],
6✔
235
                queryParamScopeSeparator,
6✔
236
                2,
6✔
237
        )
6✔
238
        var scope, attrName string
6✔
239
        if len(attrNameWithScope) == 1 {
12✔
240
                scope = model.AttrScopeInventory
6✔
241
                attrName = attrNameWithScope[0]
6✔
242
        } else {
6✔
UNCOV
243
                scope = attrNameWithScope[0]
×
244
                attrName = attrNameWithScope[1]
×
245
        }
×
246
        sort := store.Sort{AttrName: attrName, AttrScope: scope}
6✔
247
        if len(sortValArray) == 2 {
12✔
248
                sortOrder := sortValArray[sortOrderIdx]
6✔
249
                if sortOrder != sortOrderAsc && sortOrder != sortOrderDesc {
7✔
250
                        return nil, errors.New("invalid sort order")
1✔
251
                }
1✔
252
                sort.Ascending = sortOrder == sortOrderAsc
5✔
253
        }
254
        return &sort, nil
5✔
255
}
256

257
// Filter paramaters name are attributes name. Value can be prefixed
258
// with equality operator code (`eq` for =), separated from value by colon (:).
259
// Equality operator default value is `eq`
260
//
261
// eg. `attr_name1=value1` or `attr_name1=eq:value1`
262
func parseFilterParams(r *rest.Request) ([]store.Filter, error) {
25✔
263
        knownParams := []string{
25✔
264
                utils.PageName,
25✔
265
                utils.PerPageName,
25✔
266
                queryParamSort,
25✔
267
                queryParamHasGroup,
25✔
268
                queryParamGroup,
25✔
269
        }
25✔
270
        filters := make([]store.Filter, 0)
25✔
271
        var filter store.Filter
25✔
272
        for name := range r.URL.Query() {
86✔
273
                if utils.ContainsString(name, knownParams) {
110✔
274
                        continue
49✔
275
                }
276
                valueStr, err := utils.ParseQueryParmStr(r, name, false, nil)
12✔
277
                if err != nil {
12✔
UNCOV
278
                        return nil, err
×
279
                }
×
280

281
                attrNameWithScope := strings.SplitN(name, queryParamScopeSeparator, 2)
12✔
282
                var scope, attrName string
12✔
283
                if len(attrNameWithScope) == 1 {
22✔
284
                        scope = model.AttrScopeInventory
10✔
285
                        attrName = attrNameWithScope[0]
10✔
286
                } else {
12✔
287
                        scope = attrNameWithScope[0]
2✔
288
                        attrName = attrNameWithScope[1]
2✔
289
                }
2✔
290
                filter = store.Filter{AttrName: attrName, AttrScope: scope}
12✔
291

12✔
292
                // make sure we parse ':'s in value, it's either:
12✔
293
                // not there
12✔
294
                // after a valid operator specifier
12✔
295
                // or/and inside the value itself(mac, etc), in which case leave it alone
12✔
296
                sepIdx := strings.Index(valueStr, ":")
12✔
297
                if sepIdx == -1 {
16✔
298
                        filter.Value = valueStr
4✔
299
                        filter.Operator = store.Eq
4✔
300
                } else {
12✔
301
                        validOps := []string{"eq"}
8✔
302
                        for _, o := range validOps {
16✔
303
                                if valueStr[:sepIdx] == o {
12✔
304
                                        switch o {
4✔
305
                                        case "eq":
4✔
306
                                                filter.Operator = store.Eq
4✔
307
                                                filter.Value = valueStr[sepIdx+1:]
4✔
308
                                        }
309
                                        break
4✔
310
                                }
311
                        }
312

313
                        if filter.Value == "" {
12✔
314
                                filter.Value = valueStr
4✔
315
                                filter.Operator = store.Eq
4✔
316
                        }
4✔
317
                }
318

319
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
12✔
320
                if err == nil {
15✔
321
                        filter.ValueFloat = &floatValue
3✔
322
                }
3✔
323

324
                timeValue, err := time.Parse("2006-01-02T15:04:05Z", filter.Value)
12✔
325
                if err == nil {
14✔
326
                        filter.ValueTime = &timeValue
2✔
327
                }
2✔
328

329
                filters = append(filters, filter)
12✔
330
        }
331
        return filters, nil
25✔
332
}
333

334
func (i *inventoryHandlers) GetDevicesHandler(w rest.ResponseWriter, r *rest.Request) {
21✔
335
        ctx := r.Context()
21✔
336

21✔
337
        l := log.FromContext(ctx)
21✔
338

21✔
339
        page, perPage, err := utils.ParsePagination(r)
21✔
340
        if err != nil {
24✔
341
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
342
                return
3✔
343
        }
3✔
344

345
        hasGroup, err := utils.ParseQueryParmBool(r, queryParamHasGroup, false, nil)
18✔
346
        if err != nil {
19✔
347
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
348
                return
1✔
349
        }
1✔
350

351
        groupName, err := utils.ParseQueryParmStr(r, "group", false, nil)
17✔
352
        if err != nil {
17✔
UNCOV
353
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
354
                return
×
355
        }
×
356

357
        sort, err := parseSortParam(r)
17✔
358
        if err != nil {
18✔
359
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
360
                return
1✔
361
        }
1✔
362

363
        filters, err := parseFilterParams(r)
16✔
364
        if err != nil {
16✔
UNCOV
365
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
366
                return
×
367
        }
×
368

369
        ld := store.ListQuery{Skip: int((page - 1) * perPage),
16✔
370
                Limit:     int(perPage),
16✔
371
                Filters:   filters,
16✔
372
                Sort:      sort,
16✔
373
                HasGroup:  hasGroup,
16✔
374
                GroupName: groupName}
16✔
375

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

16✔
378
        if err != nil {
17✔
379
                u.RestErrWithLogInternal(w, r, l, err)
1✔
380
                return
1✔
381
        }
1✔
382

383
        hasNext := totalCount > int(page*perPage)
15✔
384
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
15✔
385
        for _, l := range links {
38✔
386
                w.Header().Add("Link", l)
23✔
387
        }
23✔
388
        // the response writer will ensure the header name is in Kebab-Pascal-Case
389
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
15✔
390
        _ = w.WriteJson(devs)
15✔
391
}
392

393
func (i *inventoryHandlers) GetDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
15✔
394
        ctx := r.Context()
15✔
395

15✔
396
        l := log.FromContext(ctx)
15✔
397

15✔
398
        deviceID := r.PathParam("id")
15✔
399

15✔
400
        dev, err := i.inventory.GetDevice(ctx, model.DeviceID(deviceID))
15✔
401
        if err != nil {
16✔
402
                u.RestErrWithLogInternal(w, r, l, err)
1✔
403
                return
1✔
404
        }
1✔
405
        if dev == nil {
15✔
406
                u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
407
                return
1✔
408
        }
1✔
409
        if dev.TagsEtag != "" {
22✔
410
                w.Header().Set("ETag", dev.TagsEtag)
9✔
411
        }
9✔
412

413
        _ = w.WriteJson(dev)
13✔
414
}
415

416
func (i *inventoryHandlers) DeleteDeviceInventoryHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
417
        ctx := r.Context()
3✔
418

3✔
419
        l := log.FromContext(ctx)
3✔
420

3✔
421
        deviceID := r.PathParam("id")
3✔
422

3✔
423
        err := i.inventory.ReplaceAttributes(ctx, model.DeviceID(deviceID),
3✔
424
                model.DeviceAttributes{}, model.AttrScopeInventory, "")
3✔
425
        if err != nil && err != store.ErrDevNotFound {
4✔
426
                u.RestErrWithLogInternal(w, r, l, err)
1✔
427
                return
1✔
428
        }
1✔
429

430
        w.WriteHeader(http.StatusNoContent)
2✔
431
}
432

433
func (i *inventoryHandlers) DeleteDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
434
        ctx := r.Context()
3✔
435
        tenantId := r.PathParam("tenant_id")
3✔
436
        if tenantId != "" {
6✔
437
                id := &midentity.Identity{
3✔
438
                        Tenant: tenantId,
3✔
439
                }
3✔
440
                ctx = midentity.WithContext(ctx, id)
3✔
441
        }
3✔
442

443
        l := log.FromContext(ctx)
3✔
444

3✔
445
        deviceID := r.PathParam("device_id")
3✔
446

3✔
447
        err := i.inventory.DeleteDevice(ctx, model.DeviceID(deviceID))
3✔
448
        if err != nil && err != store.ErrDevNotFound {
4✔
449
                u.RestErrWithLogInternal(w, r, l, err)
1✔
450
                return
1✔
451
        }
1✔
452

453
        w.WriteHeader(http.StatusNoContent)
2✔
454
}
455

456
func (i *inventoryHandlers) AddDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
85✔
457
        ctx := r.Context()
85✔
458
        tenantId := r.PathParam("tenant_id")
85✔
459
        if tenantId != "" {
94✔
460
                id := &midentity.Identity{
9✔
461
                        Tenant: tenantId,
9✔
462
                }
9✔
463
                ctx = midentity.WithContext(ctx, id)
9✔
464
        }
9✔
465

466
        l := log.FromContext(ctx)
85✔
467

85✔
468
        dev, err := parseDevice(r)
85✔
469
        if err != nil {
91✔
470
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
6✔
471
                return
6✔
472
        }
6✔
473

474
        err = dev.Attributes.Validate()
79✔
475
        if err != nil {
79✔
UNCOV
476
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
477
                return
×
478
        }
×
479

480
        err = i.inventory.AddDevice(ctx, dev)
79✔
481
        if err != nil {
80✔
482
                u.RestErrWithLogInternal(w, r, l, err)
1✔
483
                return
1✔
484
        }
1✔
485

486
        w.Header().Add("Location", "devices/"+dev.ID.String())
78✔
487
        w.WriteHeader(http.StatusCreated)
78✔
488
}
489

490
func (i *inventoryHandlers) UpdateDeviceAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
13✔
491
        ctx := r.Context()
13✔
492
        l := log.FromContext(ctx)
13✔
493
        var idata *identity.Identity
13✔
494
        if idata = identity.FromContext(ctx); idata == nil || !idata.IsDevice {
15✔
495
                u.RestErrWithLog(w, r, l, errors.New("unauthorized"), http.StatusUnauthorized)
2✔
496
                return
2✔
497
        }
2✔
498
        deviceID := model.DeviceID(idata.Subject)
11✔
499
        //extract attributes from body
11✔
500
        attrs, err := parseAttributes(r)
11✔
501
        if err != nil {
15✔
502
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
503
                return
4✔
504
        }
4✔
505
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeInventory, "")
7✔
506
}
507

508
func (i *inventoryHandlers) UpdateDeviceTagsHandler(w rest.ResponseWriter, r *rest.Request) {
18✔
509
        ctx := r.Context()
18✔
510
        l := log.FromContext(ctx)
18✔
511

18✔
512
        // get device ID from uri
18✔
513
        deviceID := model.DeviceID(r.PathParam("id"))
18✔
514
        if len(deviceID) < 1 {
18✔
UNCOV
515
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
×
516
                return
×
517
        }
×
518

519
        ifMatchHeader := r.Header.Get("If-Match")
18✔
520

18✔
521
        // extract attributes from body
18✔
522
        attrs, err := parseAttributes(r)
18✔
523
        if err != nil {
18✔
UNCOV
524
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
525
                return
×
526
        }
×
527

528
        // set scope and timestamp for tags attributes
529
        now := time.Now()
18✔
530
        for i := range attrs {
63✔
531
                attrs[i].Scope = model.AttrScopeTags
45✔
532
                if attrs[i].Timestamp == nil {
90✔
533
                        attrs[i].Timestamp = &now
45✔
534
                }
45✔
535
        }
536

537
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeTags, ifMatchHeader)
18✔
538
}
539

540
func (i *inventoryHandlers) updateDeviceAttributes(
541
        w rest.ResponseWriter,
542
        r *rest.Request,
543
        ctx context.Context,
544
        attrs model.DeviceAttributes,
545
        deviceID model.DeviceID,
546
        scope string,
547
        etag string,
548
) {
25✔
549
        l := log.FromContext(ctx)
25✔
550
        var err error
25✔
551

25✔
552
        // upsert or replace the attributes
25✔
553
        if r.Method == http.MethodPatch {
40✔
554
                err = i.inventory.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
15✔
555
        } else if r.Method == http.MethodPut {
35✔
556
                err = i.inventory.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
10✔
557
        } else {
10✔
UNCOV
558
                u.RestErrWithLog(w, r, l, errors.New("method not alllowed"), http.StatusMethodNotAllowed)
×
559
                return
×
560
        }
×
561

562
        cause := errors.Cause(err)
25✔
563
        switch cause {
25✔
UNCOV
564
        case store.ErrNoAttrName:
×
565
        case inventory.ErrTooManyAttributes:
1✔
566
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
1✔
567
                return
1✔
568
        case inventory.ErrETagDoesntMatch:
4✔
569
                u.RestErrWithInfoMsg(w, r, l, cause, http.StatusPreconditionFailed, cause.Error())
4✔
570
                return
4✔
571
        }
572
        if err != nil {
21✔
573
                u.RestErrWithLogInternal(w, r, l, err)
1✔
574
                return
1✔
575
        }
1✔
576

577
        w.WriteHeader(http.StatusOK)
19✔
578
}
579

580
func (i *inventoryHandlers) PatchDeviceAttributesInternalHandler(
581
        w rest.ResponseWriter,
582
        r *rest.Request,
583
) {
11✔
584
        ctx := r.Context()
11✔
585
        tenantId := r.PathParam("tenant_id")
11✔
586
        ctx = getTenantContext(ctx, tenantId)
11✔
587

11✔
588
        l := log.FromContext(ctx)
11✔
589

11✔
590
        deviceId := r.PathParam("device_id")
11✔
591
        if len(deviceId) < 1 {
12✔
592
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
593
                return
1✔
594
        }
1✔
595
        //extract attributes from body
596
        attrs, err := parseAttributes(r)
10✔
597
        if err != nil {
14✔
598
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
4✔
599
                return
4✔
600
        }
4✔
601
        for i, a := range attrs {
18✔
602
                a.Scope = r.PathParam("scope")
12✔
603
                if a.Name == checkInTimeParamName {
12✔
NEW
604
                        t, err := time.Parse(time.RFC3339, fmt.Sprintf("%v", a.Value))
×
NEW
605
                        if err != nil {
×
NEW
606
                                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
NEW
607
                                return
×
NEW
608
                        }
×
NEW
609
                        attrs[i].Value = t
×
610
                }
611
        }
612

613
        //upsert the attributes
614
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
6✔
615
        cause := errors.Cause(err)
6✔
616
        switch cause {
6✔
UNCOV
617
        case store.ErrNoAttrName:
×
618
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
619
                return
×
620
        }
621
        if err != nil {
7✔
622
                u.RestErrWithLogInternal(w, r, l, err)
1✔
623
                return
1✔
624
        }
1✔
625

626
        w.WriteHeader(http.StatusOK)
5✔
627
}
628

629
func (i *inventoryHandlers) DeleteDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
630
        ctx := r.Context()
7✔
631

7✔
632
        l := log.FromContext(ctx)
7✔
633

7✔
634
        deviceID := r.PathParam("id")
7✔
635
        groupName := r.PathParam("name")
7✔
636

7✔
637
        err := i.inventory.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
7✔
638
        if err != nil {
11✔
639
                cause := errors.Cause(err)
4✔
640
                if cause != nil {
8✔
641
                        if cause.Error() == store.ErrDevNotFound.Error() {
7✔
642
                                u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
3✔
643
                                return
3✔
644
                        }
3✔
645
                }
646
                u.RestErrWithLogInternal(w, r, l, err)
1✔
647
                return
1✔
648
        }
649

650
        w.WriteHeader(http.StatusNoContent)
3✔
651
}
652

653
func (i *inventoryHandlers) AddDeviceToGroupHandler(w rest.ResponseWriter, r *rest.Request) {
57✔
654
        ctx := r.Context()
57✔
655

57✔
656
        l := log.FromContext(ctx)
57✔
657

57✔
658
        devId := r.PathParam("id")
57✔
659

57✔
660
        var group InventoryApiGroup
57✔
661
        err := r.DecodeJsonPayload(&group)
57✔
662
        if err != nil {
58✔
663
                u.RestErrWithLog(
1✔
664
                        w, r, l, errors.Wrap(err, "failed to decode device group data"),
1✔
665
                        http.StatusBadRequest)
1✔
666
                return
1✔
667
        }
1✔
668

669
        if err = group.Validate(); err != nil {
59✔
670
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
671
                return
3✔
672
        }
3✔
673

674
        err = i.inventory.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
53✔
675
        if err != nil {
55✔
676
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
3✔
677
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
1✔
678
                        return
1✔
679
                }
1✔
680
                u.RestErrWithLogInternal(w, r, l, err)
1✔
681
                return
1✔
682
        }
683
        w.WriteHeader(http.StatusNoContent)
51✔
684
}
685

686
func (i *inventoryHandlers) GetDevicesByGroupHandler(w rest.ResponseWriter, r *rest.Request) {
29✔
687
        ctx := r.Context()
29✔
688

29✔
689
        l := log.FromContext(ctx)
29✔
690

29✔
691
        group := r.PathParam("name")
29✔
692

29✔
693
        page, perPage, err := utils.ParsePagination(r)
29✔
694
        if err != nil {
32✔
695
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
696
                return
3✔
697
        }
3✔
698

699
        //get one extra device to see if there's a 'next' page
700
        ids, totalCount, err := i.inventory.ListDevicesByGroup(
26✔
701
                ctx,
26✔
702
                model.GroupName(group),
26✔
703
                int((page-1)*perPage),
26✔
704
                int(perPage),
26✔
705
        )
26✔
706
        if err != nil {
32✔
707
                if err == store.ErrGroupNotFound {
11✔
708
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
5✔
709
                } else {
6✔
710
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
711
                }
1✔
712
                return
6✔
713
        }
714

715
        hasNext := totalCount > int(page*perPage)
20✔
716

20✔
717
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
20✔
718
        for _, l := range links {
43✔
719
                w.Header().Add("Link", l)
23✔
720
        }
23✔
721
        // the response writer will ensure the header name is in Kebab-Pascal-Case
722
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
20✔
723
        _ = w.WriteJson(ids)
20✔
724
}
725

726
func (i *inventoryHandlers) AppendDevicesToGroup(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
        groupName := model.GroupName(r.PathParam("name"))
5✔
731
        if err := groupName.Validate(); err != nil {
6✔
732
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
733
                return
1✔
734
        }
1✔
735

736
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
737
                u.RestErrWithLog(w, r, l,
1✔
738
                        errors.Wrap(err, "invalid payload schema"),
1✔
739
                        http.StatusBadRequest,
1✔
740
                )
1✔
741
                return
1✔
742
        } else if len(deviceIDs) == 0 {
5✔
743
                u.RestErrWithLog(w, r, l,
1✔
744
                        errors.New("no device IDs present in payload"),
1✔
745
                        http.StatusBadRequest,
1✔
746
                )
1✔
747
                return
1✔
748
        }
1✔
749
        updated, err := i.inventory.UpdateDevicesGroup(
2✔
750
                ctx, deviceIDs, groupName,
2✔
751
        )
2✔
752
        if err != nil {
3✔
753
                u.RestErrWithLogInternal(w, r, l, err)
1✔
754
                return
1✔
755
        }
1✔
756
        _ = w.WriteJson(updated)
1✔
757
}
758

759
func (i *inventoryHandlers) DeleteGroupHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
760
        ctx := r.Context()
3✔
761
        l := log.FromContext(ctx)
3✔
762

3✔
763
        groupName := model.GroupName(r.PathParam("name"))
3✔
764
        if err := groupName.Validate(); err != nil {
4✔
765
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
766
                return
1✔
767
        }
1✔
768

769
        updated, err := i.inventory.DeleteGroup(ctx, groupName)
2✔
770
        if err != nil {
3✔
771
                u.RestErrWithLogInternal(w, r, l, err)
1✔
772
                return
1✔
773
        }
1✔
774
        w.WriteHeader(http.StatusOK)
1✔
775
        _ = w.WriteJson(updated)
1✔
776
}
777

778
func (i *inventoryHandlers) ClearDevicesGroupHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
779
        var deviceIDs []model.DeviceID
5✔
780
        ctx := r.Context()
5✔
781
        l := log.FromContext(ctx)
5✔
782

5✔
783
        groupName := model.GroupName(r.PathParam("name"))
5✔
784
        if err := groupName.Validate(); err != nil {
6✔
785
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
786
                return
1✔
787
        }
1✔
788

789
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
5✔
790
                u.RestErrWithLog(w, r, l,
1✔
791
                        errors.Wrap(err, "invalid payload schema"),
1✔
792
                        http.StatusBadRequest,
1✔
793
                )
1✔
794
                return
1✔
795
        } else if len(deviceIDs) == 0 {
5✔
796
                u.RestErrWithLog(w, r, l,
1✔
797
                        errors.New("no device IDs present in payload"),
1✔
798
                        http.StatusBadRequest,
1✔
799
                )
1✔
800
                return
1✔
801
        }
1✔
802

803
        updated, err := i.inventory.UnsetDevicesGroup(ctx, deviceIDs, groupName)
2✔
804
        if err != nil {
3✔
805
                u.RestErrWithLogInternal(w, r, l, err)
1✔
806
                return
1✔
807
        }
1✔
808
        w.WriteHeader(http.StatusOK)
1✔
809
        _ = w.WriteJson(updated)
1✔
810
}
811

812
func parseDevice(r *rest.Request) (*model.Device, error) {
85✔
813
        dev := model.Device{}
85✔
814

85✔
815
        //decode body
85✔
816
        err := r.DecodeJsonPayload(&dev)
85✔
817
        if err != nil {
88✔
818
                return nil, errors.Wrap(err, "failed to decode request body")
3✔
819
        }
3✔
820

821
        if err := dev.Validate(); err != nil {
85✔
822
                return nil, err
3✔
823
        }
3✔
824

825
        return &dev, nil
79✔
826
}
827

828
func parseAttributes(r *rest.Request) (model.DeviceAttributes, error) {
39✔
829
        var attrs model.DeviceAttributes
39✔
830

39✔
831
        err := r.DecodeJsonPayload(&attrs)
39✔
832
        if err != nil {
43✔
833
                return nil, errors.Wrap(err, "failed to decode request body")
4✔
834
        }
4✔
835

836
        err = attrs.Validate()
35✔
837
        if err != nil {
39✔
838
                return nil, err
4✔
839
        }
4✔
840

841
        return attrs, nil
31✔
842
}
843

844
func (i *inventoryHandlers) GetGroupsHandler(w rest.ResponseWriter, r *rest.Request) {
9✔
845
        var fltr []model.FilterPredicate
9✔
846
        ctx := r.Context()
9✔
847

9✔
848
        l := log.FromContext(ctx)
9✔
849

9✔
850
        query := r.URL.Query()
9✔
851
        status := query.Get("status")
9✔
852
        if status != "" {
10✔
853
                fltr = []model.FilterPredicate{{
1✔
854
                        Attribute: "status",
1✔
855
                        Scope:     "identity",
1✔
856
                        Type:      "$eq",
1✔
857
                        Value:     status,
1✔
858
                }}
1✔
859
        }
1✔
860

861
        groups, err := i.inventory.ListGroups(ctx, fltr)
9✔
862
        if err != nil {
10✔
863
                u.RestErrWithLogInternal(w, r, l, err)
1✔
864
                return
1✔
865
        }
1✔
866

867
        if groups == nil {
9✔
868
                groups = []model.GroupName{}
1✔
869
        }
1✔
870

871
        _ = w.WriteJson(groups)
8✔
872
}
873

874
func (i *inventoryHandlers) GetDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
875
        ctx := r.Context()
4✔
876

4✔
877
        l := log.FromContext(ctx)
4✔
878

4✔
879
        deviceID := r.PathParam("id")
4✔
880

4✔
881
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
882
        if err != nil {
6✔
883
                if err == store.ErrDevNotFound {
3✔
884
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
885
                } else {
2✔
886
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
887
                }
1✔
888
                return
2✔
889
        }
890

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

2✔
893
        if group != "" {
3✔
894
                ret["group"] = &group
1✔
895
        }
1✔
896

897
        _ = w.WriteJson(ret)
2✔
898
}
899

900
type newTenantRequest struct {
901
        TenantID string `json:"tenant_id" valid:"required"`
902
}
903

904
func (t newTenantRequest) Validate() error {
7✔
905
        return validation.ValidateStruct(&t,
7✔
906
                validation.Field(&t.TenantID, validation.Required),
7✔
907
        )
7✔
908
}
7✔
909

910
func (i *inventoryHandlers) CreateTenantHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
911
        ctx := r.Context()
8✔
912

8✔
913
        l := log.FromContext(ctx)
8✔
914

8✔
915
        var newTenant newTenantRequest
8✔
916

8✔
917
        if err := r.DecodeJsonPayload(&newTenant); err != nil {
9✔
918
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
919
                return
1✔
920
        }
1✔
921

922
        if err := newTenant.Validate(); err != nil {
9✔
923
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
924
                return
2✔
925
        }
2✔
926

927
        err := i.inventory.CreateTenant(ctx, model.NewTenant{
5✔
928
                ID: newTenant.TenantID,
5✔
929
        })
5✔
930
        if err != nil {
6✔
931
                u.RestErrWithLogInternal(w, r, l, err)
1✔
932
                return
1✔
933
        }
1✔
934

935
        w.WriteHeader(http.StatusCreated)
4✔
936
}
937

938
func (i *inventoryHandlers) FiltersAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
939
        ctx := r.Context()
5✔
940

5✔
941
        l := log.FromContext(ctx)
5✔
942

5✔
943
        // query the database
5✔
944
        attributes, err := i.inventory.GetFiltersAttributes(ctx)
5✔
945
        if err != nil {
6✔
946
                u.RestErrWithLogInternal(w, r, l, err)
1✔
947
                return
1✔
948
        }
1✔
949

950
        // in case of nil make sure we return empty list
951
        if attributes == nil {
6✔
952
                attributes = []model.FilterAttribute{}
2✔
953
        }
2✔
954

955
        _ = w.WriteJson(attributes)
4✔
956
}
957

958
func (i *inventoryHandlers) FiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
959
        ctx := r.Context()
8✔
960

8✔
961
        l := log.FromContext(ctx)
8✔
962

8✔
963
        //extract attributes from body
8✔
964
        searchParams, err := parseSearchParams(r)
8✔
965
        if err != nil {
10✔
966
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
967
                return
2✔
968
        }
2✔
969

970
        // query the database
971
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
6✔
972
        if err != nil {
8✔
973
                if strings.Contains(err.Error(), "BadValue") {
3✔
974
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
975
                } else {
2✔
976
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
977
                }
1✔
978
                return
2✔
979
        }
980

981
        // the response writer will ensure the header name is in Kebab-Pascal-Case
982
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
4✔
983
        _ = w.WriteJson(devs)
4✔
984
}
985

986
func (i *inventoryHandlers) InternalFiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
5✔
987
        ctx := r.Context()
5✔
988

5✔
989
        l := log.FromContext(ctx)
5✔
990

5✔
991
        tenantId := r.PathParam("tenant_id")
5✔
992
        if tenantId != "" {
9✔
993
                ctx = getTenantContext(ctx, tenantId)
4✔
994
        }
4✔
995

996
        //extract attributes from body
997
        searchParams, err := parseSearchParams(r)
5✔
998
        if err != nil {
6✔
999
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1000
                return
1✔
1001
        }
1✔
1002

1003
        // query the database
1004
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
4✔
1005
        if err != nil {
6✔
1006
                if strings.Contains(err.Error(), "BadValue") {
3✔
1007
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1008
                } else {
2✔
1009
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1010
                }
1✔
1011
                return
2✔
1012
        }
1013

1014
        // the response writer will ensure the header name is in Kebab-Pascal-Case
1015
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
1016
        _ = w.WriteJson(devs)
2✔
1017
}
1018

1019
func getTenantContext(ctx context.Context, tenantId string) context.Context {
33✔
1020
        if ctx == nil {
33✔
UNCOV
1021
                ctx = context.Background()
×
1022
        }
×
1023
        if tenantId == "" {
35✔
1024
                return ctx
2✔
1025
        }
2✔
1026
        id := &midentity.Identity{
31✔
1027
                Tenant: tenantId,
31✔
1028
        }
31✔
1029

31✔
1030
        ctx = midentity.WithContext(ctx, id)
31✔
1031

31✔
1032
        return ctx
31✔
1033
}
1034

1035
func (i *inventoryHandlers) InternalDevicesStatusHandler(w rest.ResponseWriter, r *rest.Request) {
8✔
1036
        const (
8✔
1037
                StatusDecommissioned = "decommissioned"
8✔
1038
                StatusAccepted       = "accepted"
8✔
1039
                StatusRejected       = "rejected"
8✔
1040
                StatusPreauthorized  = "preauthorized"
8✔
1041
                StatusPending        = "pending"
8✔
1042
                StatusNoAuth         = "noauth"
8✔
1043
        )
8✔
1044
        var (
8✔
1045
                devices []model.DeviceUpdate
8✔
1046
                result  *model.UpdateResult
8✔
1047
        )
8✔
1048

8✔
1049
        ctx := r.Context()
8✔
1050
        l := log.FromContext(ctx)
8✔
1051

8✔
1052
        tenantID := r.PathParam("tenant_id")
8✔
1053
        ctx = getTenantContext(ctx, tenantID)
8✔
1054

8✔
1055
        status := r.PathParam("status")
8✔
1056

8✔
1057
        err := r.DecodeJsonPayload(&devices)
8✔
1058
        if err != nil {
10✔
1059
                u.RestErrWithLog(w, r, l, errors.Wrap(err, "cant parse devices"), http.StatusBadRequest)
2✔
1060
                return
2✔
1061
        }
2✔
1062

1063
        switch status {
6✔
1064
        case StatusAccepted, StatusPreauthorized,
1065
                StatusPending, StatusRejected,
1066
                StatusNoAuth:
5✔
1067
                // Update statuses
5✔
1068
                attrs := model.DeviceAttributes{{
5✔
1069
                        Name:  "status",
5✔
1070
                        Scope: model.AttrScopeIdentity,
5✔
1071
                        Value: status,
5✔
1072
                }}
5✔
1073
                result, err = i.inventory.UpsertDevicesStatuses(ctx, devices, attrs)
5✔
UNCOV
1074
        case StatusDecommissioned:
×
1075
                // Delete Inventory
×
1076
                result, err = i.inventory.DeleteDevices(ctx, getIdsFromDevices(devices))
×
1077
        default:
1✔
1078
                // Unrecognized status
1✔
1079
                u.RestErrWithLog(w, r, l,
1✔
1080
                        errors.Errorf("unrecognized status: %s", status),
1✔
1081
                        http.StatusNotFound,
1✔
1082
                )
1✔
1083
                return
1✔
1084
        }
1085
        if err == store.ErrWriteConflict {
6✔
1086
                u.RestErrWithLog(w, r, l, err, http.StatusConflict)
1✔
1087
                return
1✔
1088
        } else if err != nil {
6✔
1089
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1090
                return
1✔
1091
        }
1✔
1092

1093
        w.WriteHeader(http.StatusOK)
3✔
1094
        _ = w.WriteJson(result)
3✔
1095
}
1096

1097
func (i *inventoryHandlers) GetDeviceGroupsInternalHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
1098
        ctx := r.Context()
4✔
1099

4✔
1100
        l := log.FromContext(ctx)
4✔
1101

4✔
1102
        tenantId := r.PathParam("tenant_id")
4✔
1103
        ctx = getTenantContext(ctx, tenantId)
4✔
1104

4✔
1105
        deviceID := r.PathParam("device_id")
4✔
1106
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
4✔
1107
        if err != nil {
6✔
1108
                if err == store.ErrDevNotFound {
3✔
1109
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
1110
                } else {
2✔
1111
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1112
                }
1✔
1113
                return
2✔
1114
        }
1115

1116
        res := model.DeviceGroups{}
2✔
1117
        if group != "" {
3✔
1118
                res.Groups = append(res.Groups, string(group))
1✔
1119
        }
1✔
1120

1121
        _ = w.WriteJson(res)
2✔
1122
}
1123

1124
func (i *inventoryHandlers) ReindexDeviceDataHandler(w rest.ResponseWriter, r *rest.Request) {
6✔
1125
        ctx := r.Context()
6✔
1126
        tenantId := r.PathParam("tenant_id")
6✔
1127
        ctx = getTenantContext(ctx, tenantId)
6✔
1128

6✔
1129
        l := log.FromContext(ctx)
6✔
1130

6✔
1131
        deviceId := r.PathParam("device_id")
6✔
1132
        if len(deviceId) < 1 {
7✔
1133
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
1134
                return
1✔
1135
        }
1✔
1136

1137
        serviceName, err := utils.ParseQueryParmStr(r, "service", false, nil)
5✔
1138
        // inventory service accepts only reindex requests from devicemonitor
5✔
1139
        if err != nil || serviceName != "devicemonitor" {
6✔
1140
                u.RestErrWithLog(w, r, l, errors.New("unsupported service"), http.StatusBadRequest)
1✔
1141
                return
1✔
1142
        }
1✔
1143

1144
        // check devicemonitor alerts
1145
        alertsCount, err := i.inventory.CheckAlerts(ctx, deviceId)
4✔
1146
        if err != nil {
5✔
1147
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1148
                return
1✔
1149
        }
1✔
1150

1151
        alertsPresent := false
3✔
1152
        if alertsCount > 0 {
4✔
1153
                alertsPresent = true
1✔
1154
        }
1✔
1155
        attrs := model.DeviceAttributes{
3✔
1156
                model.DeviceAttribute{
3✔
1157
                        Name:  model.AttrNameNumberOfAlerts,
3✔
1158
                        Scope: model.AttrScopeMonitor,
3✔
1159
                        Value: alertsCount,
3✔
1160
                },
3✔
1161
                model.DeviceAttribute{
3✔
1162
                        Name:  model.AttrNameAlerts,
3✔
1163
                        Scope: model.AttrScopeMonitor,
3✔
1164
                        Value: alertsPresent,
3✔
1165
                },
3✔
1166
        }
3✔
1167

3✔
1168
        // upsert monitor attributes
3✔
1169
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
3✔
1170
        cause := errors.Cause(err)
3✔
1171
        switch cause {
3✔
UNCOV
1172
        case store.ErrNoAttrName:
×
1173
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
1174
                return
×
1175
        }
1176
        if err != nil {
4✔
1177
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1178
                return
1✔
1179
        }
1✔
1180

1181
        w.WriteHeader(http.StatusOK)
2✔
1182
}
1183

UNCOV
1184
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
×
1185
        ids := make([]model.DeviceID, len(devices))
×
1186
        for i, dev := range devices {
×
1187
                ids[i] = dev.Id
×
1188
        }
×
1189
        return ids
×
1190
}
1191

1192
func parseSearchParams(r *rest.Request) (*model.SearchParams, error) {
19✔
1193
        var searchParams model.SearchParams
19✔
1194

19✔
1195
        if err := r.DecodeJsonPayload(&searchParams); err != nil {
20✔
1196
                return nil, errors.Wrap(err, "failed to decode request body")
1✔
1197
        }
1✔
1198

1199
        if searchParams.Page < 1 {
19✔
1200
                searchParams.Page = utils.PageDefault
1✔
1201
        }
1✔
1202
        if searchParams.PerPage < 1 {
19✔
1203
                searchParams.PerPage = utils.PerPageDefault
1✔
1204
        }
1✔
1205

1206
        if err := searchParams.Validate(); err != nil {
23✔
1207
                return nil, err
5✔
1208
        }
5✔
1209

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