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

mendersoftware / mender-server / 1841158960

28 May 2025 02:31PM UTC coverage: 65.865% (+0.08%) from 65.783%
1841158960

Pull #696

gitlab-ci

alfrunes
test(deviceconnect): Add error context to failing test

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #696: ci: Debug mongodb in backend unit tests

32569 of 49448 relevant lines covered (65.87%)

1.39 hits per line

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

95.76
/backend/services/inventory/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/mender-server/pkg/accesslog"
30
        "github.com/mendersoftware/mender-server/pkg/identity"
31
        midentity "github.com/mendersoftware/mender-server/pkg/identity"
32
        "github.com/mendersoftware/mender-server/pkg/log"
33
        "github.com/mendersoftware/mender-server/pkg/requestid"
34
        "github.com/mendersoftware/mender-server/pkg/requestlog"
35
        "github.com/mendersoftware/mender-server/pkg/rest_utils"
36
        u "github.com/mendersoftware/mender-server/pkg/rest_utils"
37

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

44
const (
45
        // legacy endpoints
46
        legacyUriDevices       = "/api/0.1.0/devices"
47
        legacyUriDevice        = "/api/0.1.0/devices/#id"
48
        legacyUriDeviceTags    = "/api/0.1.0/devices/#id/tags"
49
        legacyUriDeviceGroups  = "/api/0.1.0/devices/#id/group"
50
        legacyUriDeviceGroup   = "/api/0.1.0/devices/#id/group/#name"
51
        legacyUriAttributes    = "/api/0.1.0/attributes"
52
        legacyUriGroups        = "/api/0.1.0/groups"
53
        legacyUriGroupsName    = "/api/0.1.0/groups/#name"
54
        legacyUriGroupsDevices = "/api/0.1.0/groups/#name/devices"
55

56
        apiUrlManagmentV1 = "/api/management/v1/inventory"
57
        uriDevices        = apiUrlManagmentV1 + "/devices"
58
        uriDevice         = apiUrlManagmentV1 + "/devices/#id"
59
        uriDeviceTags     = apiUrlManagmentV1 + "/devices/#id/tags"
60
        uriDeviceGroups   = apiUrlManagmentV1 + "/devices/#id/group"
61
        uriDeviceGroup    = apiUrlManagmentV1 + "/devices/#id/group/#name"
62
        uriGroups         = apiUrlManagmentV1 + "/groups"
63
        uriGroupsName     = apiUrlManagmentV1 + "/groups/#name"
64
        uriGroupsDevices  = apiUrlManagmentV1 + "/groups/#name/devices"
65
        uriAttributes     = "/api/devices/v1/inventory/attributes"
66

67
        apiUrlInternalV1         = "/api/internal/v1/inventory"
68
        uriInternalAlive         = apiUrlInternalV1 + "/alive"
69
        uriInternalHealth        = apiUrlInternalV1 + "/health"
70
        uriInternalTenants       = apiUrlInternalV1 + "/tenants"
71
        uriInternalDevices       = apiUrlInternalV1 + "/tenants/#tenant_id/devices"
72
        urlInternalDevicesStatus = apiUrlInternalV1 + "/tenants/#tenant_id/devices/status/#status"
73
        uriInternalDeviceDetails = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id"
74
        uriInternalDeviceGroups  = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id/groups"
75
        urlInternalAttributes    = apiUrlInternalV1 +
76
                "/tenants/#tenant_id/device/#device_id/attribute/scope/#scope"
77
        urlInternalReindex   = apiUrlInternalV1 + "/tenants/#tenant_id/devices/#device_id/reindex"
78
        apiUrlManagementV2   = "/api/management/v2/inventory"
79
        urlFiltersAttributes = apiUrlManagementV2 + "/filters/attributes"
80
        urlFiltersSearch     = apiUrlManagementV2 + "/filters/search"
81

82
        apiUrlInternalV2         = "/api/internal/v2/inventory"
83
        urlInternalFiltersSearch = apiUrlInternalV2 + "/tenants/#tenant_id/filters/search"
84

85
        hdrTotalCount = "X-Total-Count"
86
)
87

88
const (
89
        queryParamGroup          = "group"
90
        queryParamSort           = "sort"
91
        queryParamHasGroup       = "has_group"
92
        queryParamValueSeparator = ":"
93
        queryParamScopeSeparator = "/"
94
        sortOrderAsc             = "asc"
95
        sortOrderDesc            = "desc"
96
        sortAttributeNameIdx     = 0
97
        sortOrderIdx             = 1
98
)
99

100
const (
101
        DefaultTimeout = time.Second * 10
102
)
103

104
const (
105
        checkInTimeParamName  = "check_in_time"
106
        checkInTimeParamScope = "system"
107
)
108

109
// model of device's group name response at /devices/:id/group endpoint
110
type InventoryApiGroup struct {
111
        Group model.GroupName `json:"group"`
112
}
113

114
func (g InventoryApiGroup) Validate() error {
3✔
115
        return g.Group.Validate()
3✔
116
}
3✔
117

118
type inventoryHandlers struct {
119
        inventory inventory.InventoryApp
120
}
121

122
// return an ApiHandler for device admission app
123
func NewInventoryApiHandlers(i inventory.InventoryApp) ApiHandler {
3✔
124
        return &inventoryHandlers{
3✔
125
                inventory: i,
3✔
126
        }
3✔
127
}
3✔
128

129
func wrapRoutes(middleware rest.Middleware, routes ...*rest.Route) []*rest.Route {
3✔
130
        for _, route := range routes {
6✔
131
                route.Func = middleware.MiddlewareFunc(route.Func)
3✔
132
        }
3✔
133
        return routes
3✔
134
}
135

136
func (i *inventoryHandlers) Build() (http.Handler, error) {
3✔
137
        //this will override the framework's error resp to the desired one:
3✔
138
        // {"error": "msg"}
3✔
139
        // instead of:
3✔
140
        // {"Error": "msg"}
3✔
141
        rest.ErrorFieldName = "error"
3✔
142

3✔
143
        api := rest.NewApi()
3✔
144

3✔
145
        api.Use(
3✔
146
                &requestlog.RequestLogMiddleware{},
3✔
147
                &requestid.RequestIdMiddleware{},
3✔
148
                &accesslog.AccessLogMiddleware{
3✔
149
                        Format: accesslog.SimpleLogFormat,
3✔
150
                        DisableLog: func(statusCode int, r *rest.Request) bool {
6✔
151
                                if r.URL.Path == uriInternalAlive ||
3✔
152
                                        r.URL.Path == uriInternalHealth &&
3✔
153
                                                statusCode < 300 {
5✔
154
                                        // Skips the health/liveliness probes
2✔
155
                                        return true
2✔
156
                                }
2✔
157
                                return false
3✔
158
                        },
159
                },
160
                &rest.ContentTypeCheckerMiddleware{},
161
        )
162
        internalRoutes := []*rest.Route{
3✔
163
                rest.Get(uriInternalAlive, i.LivelinessHandler),
3✔
164
                rest.Get(uriInternalHealth, i.HealthCheckHandler),
3✔
165

3✔
166
                rest.Patch(urlInternalAttributes, i.PatchDeviceAttributesInternalHandler),
3✔
167
                rest.Post(urlInternalReindex, i.ReindexDeviceDataHandler),
3✔
168

3✔
169
                rest.Post(uriInternalTenants, i.CreateTenantHandler),
3✔
170
                rest.Post(uriInternalDevices, i.AddDeviceHandler),
3✔
171
                rest.Delete(uriInternalDeviceDetails, i.DeleteDeviceHandler),
3✔
172
                rest.Post(urlInternalDevicesStatus, i.InternalDevicesStatusHandler),
3✔
173
                rest.Get(uriInternalDeviceGroups, i.GetDeviceGroupsInternalHandler),
3✔
174
                rest.Post(urlInternalFiltersSearch, i.InternalFiltersSearchHandler),
3✔
175
        }
3✔
176

3✔
177
        publicRoutes := AutogenOptionsRoutes([]*rest.Route{
3✔
178
                rest.Get(uriDevices, i.GetDevicesHandler),
3✔
179
                rest.Get(uriDevice, i.GetDeviceHandler),
3✔
180
                rest.Delete(uriDevice, i.DeleteDeviceInventoryHandler),
3✔
181
                rest.Delete(uriDeviceGroup, i.DeleteDeviceGroupHandler),
3✔
182
                rest.Delete(uriGroupsName, i.DeleteGroupHandler),
3✔
183
                rest.Delete(uriGroupsDevices, i.ClearDevicesGroupHandler),
3✔
184
                rest.Patch(uriAttributes, i.UpdateDeviceAttributesHandler),
3✔
185
                rest.Put(uriAttributes, i.UpdateDeviceAttributesHandler),
3✔
186
                rest.Put(uriDeviceGroups, i.AddDeviceToGroupHandler),
3✔
187
                rest.Patch(uriGroupsDevices, i.AppendDevicesToGroup),
3✔
188
                rest.Put(uriDeviceTags, i.UpdateDeviceTagsHandler),
3✔
189
                rest.Patch(uriDeviceTags, i.UpdateDeviceTagsHandler),
3✔
190

3✔
191
                rest.Get(uriDeviceGroups, i.GetDeviceGroupHandler),
3✔
192
                rest.Get(uriGroups, i.GetGroupsHandler),
3✔
193
                rest.Get(uriGroupsDevices, i.GetDevicesByGroupHandler),
3✔
194

3✔
195
                // legacy endpoints
3✔
196
                rest.Get(legacyUriDevices, i.GetDevicesHandler),
3✔
197
                rest.Get(legacyUriDevice, i.GetDeviceHandler),
3✔
198
                rest.Delete(legacyUriDevice, i.DeleteDeviceInventoryHandler),
3✔
199
                rest.Delete(legacyUriDeviceGroup, i.DeleteDeviceGroupHandler),
3✔
200
                rest.Delete(legacyUriGroupsName, i.DeleteGroupHandler),
3✔
201
                rest.Delete(legacyUriGroupsDevices, i.ClearDevicesGroupHandler),
3✔
202
                rest.Patch(legacyUriAttributes, i.UpdateDeviceAttributesHandler),
3✔
203
                rest.Put(legacyUriAttributes, i.UpdateDeviceAttributesHandler),
3✔
204
                rest.Put(legacyUriDeviceGroups, i.AddDeviceToGroupHandler),
3✔
205
                rest.Patch(legacyUriGroupsDevices, i.AppendDevicesToGroup),
3✔
206
                rest.Put(legacyUriDeviceTags, i.UpdateDeviceTagsHandler),
3✔
207
                rest.Patch(legacyUriDeviceTags, i.UpdateDeviceTagsHandler),
3✔
208
                rest.Get(legacyUriDeviceGroups, i.GetDeviceGroupHandler),
3✔
209
                rest.Get(legacyUriGroups, i.GetGroupsHandler),
3✔
210
                rest.Get(legacyUriGroupsDevices, i.GetDevicesByGroupHandler),
3✔
211

3✔
212
                rest.Get(urlFiltersAttributes, i.FiltersAttributesHandler),
3✔
213
                rest.Post(urlFiltersSearch, i.FiltersSearchHandler),
3✔
214
        }, AllowHeaderOptionsGenerator)
3✔
215
        publicRoutes = wrapRoutes(&identity.IdentityMiddleware{
3✔
216
                UpdateLogger: true,
3✔
217
        }, publicRoutes...)
3✔
218
        routes := append(internalRoutes, publicRoutes...)
3✔
219

3✔
220
        app, err := rest.MakeRouter(routes...)
3✔
221
        if err != nil {
3✔
222
                return nil, errors.Wrap(err, "failed to create router")
×
223
        }
×
224
        api.SetApp(app)
3✔
225

3✔
226
        return api.MakeHandler(), nil
3✔
227

228
}
229

230
func (i *inventoryHandlers) LivelinessHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
231
        w.WriteHeader(http.StatusNoContent)
2✔
232
}
2✔
233

234
func (i *inventoryHandlers) HealthCheckHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
235
        ctx := r.Context()
2✔
236
        l := log.FromContext(ctx)
2✔
237

2✔
238
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
2✔
239
        defer cancel()
2✔
240

2✔
241
        err := i.inventory.HealthCheck(ctx)
2✔
242
        if err != nil {
3✔
243
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
244
                return
1✔
245
        }
1✔
246

247
        w.WriteHeader(http.StatusNoContent)
2✔
248
}
249

250
// `sort` paramater value is an attribute name with optional direction (desc or asc)
251
// separated by colon (:)
252
//
253
// eg. `sort=attr_name1` or `sort=attr_name1:asc`
254
func parseSortParam(r *rest.Request) (*store.Sort, error) {
3✔
255
        sortStr, err := utils.ParseQueryParmStr(r, queryParamSort, false, nil)
3✔
256
        if err != nil {
3✔
257
                return nil, err
×
258
        }
×
259
        if sortStr == "" {
6✔
260
                return nil, nil
3✔
261
        }
3✔
262
        sortValArray := strings.Split(sortStr, queryParamValueSeparator)
2✔
263
        attrNameWithScope := strings.SplitN(
2✔
264
                sortValArray[sortAttributeNameIdx],
2✔
265
                queryParamScopeSeparator,
2✔
266
                2,
2✔
267
        )
2✔
268
        var scope, attrName string
2✔
269
        if len(attrNameWithScope) == 1 {
4✔
270
                scope = model.AttrScopeInventory
2✔
271
                attrName = attrNameWithScope[0]
2✔
272
        } else {
2✔
273
                scope = attrNameWithScope[0]
×
274
                attrName = attrNameWithScope[1]
×
275
        }
×
276
        sort := store.Sort{AttrName: attrName, AttrScope: scope}
2✔
277
        if len(sortValArray) == 2 {
4✔
278
                sortOrder := sortValArray[sortOrderIdx]
2✔
279
                if sortOrder != sortOrderAsc && sortOrder != sortOrderDesc {
3✔
280
                        return nil, errors.New("invalid sort order")
1✔
281
                }
1✔
282
                sort.Ascending = sortOrder == sortOrderAsc
2✔
283
        }
284
        return &sort, nil
2✔
285
}
286

287
// Filter paramaters name are attributes name. Value can be prefixed
288
// with equality operator code (`eq` for =), separated from value by colon (:).
289
// Equality operator default value is `eq`
290
//
291
// eg. `attr_name1=value1` or `attr_name1=eq:value1`
292
func parseFilterParams(r *rest.Request) ([]store.Filter, error) {
3✔
293
        knownParams := []string{
3✔
294
                utils.PageName,
3✔
295
                utils.PerPageName,
3✔
296
                queryParamSort,
3✔
297
                queryParamHasGroup,
3✔
298
                queryParamGroup,
3✔
299
        }
3✔
300
        filters := make([]store.Filter, 0)
3✔
301
        var filter store.Filter
3✔
302
        for name := range r.URL.Query() {
6✔
303
                if utils.ContainsString(name, knownParams) {
6✔
304
                        continue
3✔
305
                }
306
                valueStr, err := utils.ParseQueryParmStr(r, name, false, nil)
3✔
307
                if err != nil {
3✔
308
                        return nil, err
×
309
                }
×
310

311
                attrNameWithScope := strings.SplitN(name, queryParamScopeSeparator, 2)
3✔
312
                var scope, attrName string
3✔
313
                if len(attrNameWithScope) == 1 {
6✔
314
                        scope = model.AttrScopeInventory
3✔
315
                        attrName = attrNameWithScope[0]
3✔
316
                } else {
4✔
317
                        scope = attrNameWithScope[0]
1✔
318
                        attrName = attrNameWithScope[1]
1✔
319
                }
1✔
320
                filter = store.Filter{AttrName: attrName, AttrScope: scope}
3✔
321

3✔
322
                // make sure we parse ':'s in value, it's either:
3✔
323
                // not there
3✔
324
                // after a valid operator specifier
3✔
325
                // or/and inside the value itself(mac, etc), in which case leave it alone
3✔
326
                sepIdx := strings.Index(valueStr, ":")
3✔
327
                if sepIdx == -1 {
5✔
328
                        filter.Value = valueStr
2✔
329
                        filter.Operator = store.Eq
2✔
330
                } else {
4✔
331
                        validOps := []string{"eq"}
2✔
332
                        for _, o := range validOps {
4✔
333
                                if valueStr[:sepIdx] == o {
3✔
334
                                        switch o {
1✔
335
                                        case "eq":
1✔
336
                                                filter.Operator = store.Eq
1✔
337
                                                filter.Value = valueStr[sepIdx+1:]
1✔
338
                                        }
339
                                        break
1✔
340
                                }
341
                        }
342

343
                        if filter.Value == "" {
4✔
344
                                filter.Value = valueStr
2✔
345
                                filter.Operator = store.Eq
2✔
346
                        }
2✔
347
                }
348

349
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
3✔
350
                if err == nil {
5✔
351
                        filter.ValueFloat = &floatValue
2✔
352
                }
2✔
353

354
                timeValue, err := time.Parse("2006-01-02T15:04:05Z", filter.Value)
3✔
355
                if err == nil {
4✔
356
                        filter.ValueTime = &timeValue
1✔
357
                }
1✔
358

359
                filters = append(filters, filter)
3✔
360
        }
361
        return filters, nil
3✔
362
}
363

364
func (i *inventoryHandlers) GetDevicesHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
365
        ctx := r.Context()
3✔
366

3✔
367
        l := log.FromContext(ctx)
3✔
368

3✔
369
        page, perPage, err := utils.ParsePagination(r)
3✔
370
        if err != nil {
4✔
371
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
372
                return
1✔
373
        }
1✔
374

375
        hasGroup, err := utils.ParseQueryParmBool(r, queryParamHasGroup, false, nil)
3✔
376
        if err != nil {
4✔
377
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
378
                return
1✔
379
        }
1✔
380

381
        groupName, err := utils.ParseQueryParmStr(r, "group", false, nil)
3✔
382
        if err != nil {
3✔
383
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
384
                return
×
385
        }
×
386

387
        sort, err := parseSortParam(r)
3✔
388
        if err != nil {
4✔
389
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
390
                return
1✔
391
        }
1✔
392

393
        filters, err := parseFilterParams(r)
3✔
394
        if err != nil {
3✔
395
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
396
                return
×
397
        }
×
398

399
        ld := store.ListQuery{Skip: int((page - 1) * perPage),
3✔
400
                Limit:     int(perPage),
3✔
401
                Filters:   filters,
3✔
402
                Sort:      sort,
3✔
403
                HasGroup:  hasGroup,
3✔
404
                GroupName: groupName}
3✔
405

3✔
406
        devs, totalCount, err := i.inventory.ListDevices(ctx, ld)
3✔
407

3✔
408
        if err != nil {
4✔
409
                u.RestErrWithLogInternal(w, r, l, err)
1✔
410
                return
1✔
411
        }
1✔
412

413
        hasNext := totalCount > int(page*perPage)
3✔
414
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
3✔
415
        for _, l := range links {
6✔
416
                w.Header().Add("Link", l)
3✔
417
        }
3✔
418
        // the response writer will ensure the header name is in Kebab-Pascal-Case
419
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
420
        _ = w.WriteJson(devs)
3✔
421
}
422

423
func (i *inventoryHandlers) GetDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
424
        ctx := r.Context()
3✔
425

3✔
426
        l := log.FromContext(ctx)
3✔
427

3✔
428
        deviceID := r.PathParam("id")
3✔
429

3✔
430
        dev, err := i.inventory.GetDevice(ctx, model.DeviceID(deviceID))
3✔
431
        if err != nil {
4✔
432
                u.RestErrWithLogInternal(w, r, l, err)
1✔
433
                return
1✔
434
        }
1✔
435
        if dev == nil {
5✔
436
                u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
2✔
437
                return
2✔
438
        }
2✔
439
        if dev.TagsEtag != "" {
4✔
440
                w.Header().Set("ETag", dev.TagsEtag)
1✔
441
        }
1✔
442

443
        _ = w.WriteJson(dev)
3✔
444
}
445

446
func (i *inventoryHandlers) DeleteDeviceInventoryHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
447
        ctx := r.Context()
1✔
448

1✔
449
        l := log.FromContext(ctx)
1✔
450

1✔
451
        deviceID := r.PathParam("id")
1✔
452

1✔
453
        err := i.inventory.ReplaceAttributes(ctx, model.DeviceID(deviceID),
1✔
454
                model.DeviceAttributes{}, model.AttrScopeInventory, "")
1✔
455
        if err != nil && err != store.ErrDevNotFound {
2✔
456
                u.RestErrWithLogInternal(w, r, l, err)
1✔
457
                return
1✔
458
        }
1✔
459

460
        w.WriteHeader(http.StatusNoContent)
1✔
461
}
462

463
func (i *inventoryHandlers) DeleteDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
464
        ctx := r.Context()
2✔
465
        tenantId := r.PathParam("tenant_id")
2✔
466
        if tenantId != "" {
4✔
467
                id := &midentity.Identity{
2✔
468
                        Tenant: tenantId,
2✔
469
                }
2✔
470
                ctx = midentity.WithContext(ctx, id)
2✔
471
        }
2✔
472

473
        l := log.FromContext(ctx)
2✔
474

2✔
475
        deviceID := r.PathParam("device_id")
2✔
476

2✔
477
        err := i.inventory.DeleteDevice(ctx, model.DeviceID(deviceID))
2✔
478
        if err != nil && err != store.ErrDevNotFound {
3✔
479
                u.RestErrWithLogInternal(w, r, l, err)
1✔
480
                return
1✔
481
        }
1✔
482

483
        w.WriteHeader(http.StatusNoContent)
2✔
484
}
485

486
func (i *inventoryHandlers) AddDeviceHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
487
        ctx := r.Context()
3✔
488
        tenantId := r.PathParam("tenant_id")
3✔
489
        if tenantId != "" {
5✔
490
                id := &midentity.Identity{
2✔
491
                        Tenant: tenantId,
2✔
492
                }
2✔
493
                ctx = midentity.WithContext(ctx, id)
2✔
494
        }
2✔
495

496
        l := log.FromContext(ctx)
3✔
497

3✔
498
        dev, err := parseDevice(r)
3✔
499
        if err != nil {
5✔
500
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
501
                return
2✔
502
        }
2✔
503

504
        err = dev.Attributes.Validate()
3✔
505
        if err != nil {
3✔
506
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
507
                return
×
508
        }
×
509

510
        err = i.inventory.AddDevice(ctx, dev)
3✔
511
        if err != nil {
4✔
512
                u.RestErrWithLogInternal(w, r, l, err)
1✔
513
                return
1✔
514
        }
1✔
515

516
        w.Header().Add("Location", "devices/"+dev.ID.String())
3✔
517
        w.WriteHeader(http.StatusCreated)
3✔
518
}
519

520
func (i *inventoryHandlers) UpdateDeviceAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
521
        ctx := r.Context()
2✔
522
        l := log.FromContext(ctx)
2✔
523
        var idata *identity.Identity
2✔
524
        if idata = identity.FromContext(ctx); idata == nil || !idata.IsDevice {
3✔
525
                u.RestErrWithLog(w, r, l, errors.New("unauthorized"), http.StatusUnauthorized)
1✔
526
                return
1✔
527
        }
1✔
528
        deviceID := model.DeviceID(idata.Subject)
2✔
529
        //extract attributes from body
2✔
530
        attrs, err := parseAttributes(r)
2✔
531
        if err != nil {
4✔
532
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
533
                return
2✔
534
        }
2✔
535
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeInventory, "")
2✔
536
}
537

538
func (i *inventoryHandlers) UpdateDeviceTagsHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
539
        ctx := r.Context()
2✔
540
        l := log.FromContext(ctx)
2✔
541

2✔
542
        // get device ID from uri
2✔
543
        deviceID := model.DeviceID(r.PathParam("id"))
2✔
544
        if len(deviceID) < 1 {
2✔
545
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
×
546
                return
×
547
        }
×
548

549
        ifMatchHeader := r.Header.Get("If-Match")
2✔
550

2✔
551
        // extract attributes from body
2✔
552
        attrs, err := parseAttributes(r)
2✔
553
        if err != nil {
2✔
554
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
555
                return
×
556
        }
×
557

558
        // set scope and timestamp for tags attributes
559
        now := time.Now()
2✔
560
        for i := range attrs {
4✔
561
                attrs[i].Scope = model.AttrScopeTags
2✔
562
                if attrs[i].Timestamp == nil {
4✔
563
                        attrs[i].Timestamp = &now
2✔
564
                }
2✔
565
        }
566

567
        i.updateDeviceAttributes(w, r, ctx, attrs, deviceID, model.AttrScopeTags, ifMatchHeader)
2✔
568
}
569

570
func (i *inventoryHandlers) updateDeviceAttributes(
571
        w rest.ResponseWriter,
572
        r *rest.Request,
573
        ctx context.Context,
574
        attrs model.DeviceAttributes,
575
        deviceID model.DeviceID,
576
        scope string,
577
        etag string,
578
) {
3✔
579
        l := log.FromContext(ctx)
3✔
580
        var err error
3✔
581

3✔
582
        // upsert or replace the attributes
3✔
583
        if r.Method == http.MethodPatch {
6✔
584
                err = i.inventory.UpsertAttributesWithUpdated(ctx, deviceID, attrs, scope, etag)
3✔
585
        } else if r.Method == http.MethodPut {
7✔
586
                err = i.inventory.ReplaceAttributes(ctx, deviceID, attrs, scope, etag)
2✔
587
        } else {
2✔
588
                u.RestErrWithLog(w, r, l, errors.New("method not alllowed"), http.StatusMethodNotAllowed)
×
589
                return
×
590
        }
×
591

592
        cause := errors.Cause(err)
3✔
593
        switch cause {
3✔
594
        case store.ErrNoAttrName:
×
595
        case inventory.ErrTooManyAttributes:
1✔
596
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
1✔
597
                return
1✔
598
        case inventory.ErrETagDoesntMatch:
2✔
599
                u.RestErrWithInfoMsg(w, r, l, cause, http.StatusPreconditionFailed, cause.Error())
2✔
600
                return
2✔
601
        }
602
        if err != nil {
4✔
603
                u.RestErrWithLogInternal(w, r, l, err)
1✔
604
                return
1✔
605
        }
1✔
606

607
        w.WriteHeader(http.StatusOK)
3✔
608
}
609

610
func (i *inventoryHandlers) PatchDeviceAttributesInternalHandler(
611
        w rest.ResponseWriter,
612
        r *rest.Request,
613
) {
2✔
614
        ctx := r.Context()
2✔
615
        tenantId := r.PathParam("tenant_id")
2✔
616
        ctx = getTenantContext(ctx, tenantId)
2✔
617

2✔
618
        l := log.FromContext(ctx)
2✔
619

2✔
620
        deviceId := r.PathParam("device_id")
2✔
621
        if len(deviceId) < 1 {
3✔
622
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
623
                return
1✔
624
        }
1✔
625
        //extract attributes from body
626
        attrs, err := parseAttributes(r)
2✔
627
        if err != nil {
4✔
628
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
629
                return
2✔
630
        }
2✔
631
        for i := range attrs {
4✔
632
                attrs[i].Scope = r.PathParam("scope")
2✔
633
                if attrs[i].Name == checkInTimeParamName && attrs[i].Scope == checkInTimeParamScope {
3✔
634
                        t, err := time.Parse(time.RFC3339, fmt.Sprintf("%v", attrs[i].Value))
1✔
635
                        if err != nil {
1✔
636
                                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
637
                                return
×
638
                        }
×
639
                        attrs[i].Value = t
1✔
640
                }
641
        }
642

643
        //upsert the attributes
644
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
2✔
645
        cause := errors.Cause(err)
2✔
646
        switch cause {
2✔
647
        case store.ErrNoAttrName:
×
648
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
649
                return
×
650
        }
651
        if err != nil {
3✔
652
                u.RestErrWithLogInternal(w, r, l, err)
1✔
653
                return
1✔
654
        }
1✔
655

656
        w.WriteHeader(http.StatusOK)
2✔
657
}
658

659
func (i *inventoryHandlers) DeleteDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
660
        ctx := r.Context()
2✔
661

2✔
662
        l := log.FromContext(ctx)
2✔
663

2✔
664
        deviceID := r.PathParam("id")
2✔
665
        groupName := r.PathParam("name")
2✔
666

2✔
667
        err := i.inventory.UnsetDeviceGroup(ctx, model.DeviceID(deviceID), model.GroupName(groupName))
2✔
668
        if err != nil {
4✔
669
                cause := errors.Cause(err)
2✔
670
                if cause != nil {
4✔
671
                        if cause.Error() == store.ErrDevNotFound.Error() {
4✔
672
                                u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
2✔
673
                                return
2✔
674
                        }
2✔
675
                }
676
                u.RestErrWithLogInternal(w, r, l, err)
1✔
677
                return
1✔
678
        }
679

680
        w.WriteHeader(http.StatusNoContent)
2✔
681
}
682

683
func (i *inventoryHandlers) AddDeviceToGroupHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
684
        ctx := r.Context()
3✔
685

3✔
686
        l := log.FromContext(ctx)
3✔
687

3✔
688
        devId := r.PathParam("id")
3✔
689

3✔
690
        var group InventoryApiGroup
3✔
691
        err := r.DecodeJsonPayload(&group)
3✔
692
        if err != nil {
4✔
693
                u.RestErrWithLog(
1✔
694
                        w, r, l, errors.Wrap(err, "failed to decode device group data"),
1✔
695
                        http.StatusBadRequest)
1✔
696
                return
1✔
697
        }
1✔
698

699
        if err = group.Validate(); err != nil {
4✔
700
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
701
                return
1✔
702
        }
1✔
703

704
        err = i.inventory.UpdateDeviceGroup(ctx, model.DeviceID(devId), model.GroupName(group.Group))
3✔
705
        if err != nil {
4✔
706
                if cause := errors.Cause(err); cause != nil && cause == store.ErrDevNotFound {
2✔
707
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
1✔
708
                        return
1✔
709
                }
1✔
710
                u.RestErrWithLogInternal(w, r, l, err)
1✔
711
                return
1✔
712
        }
713
        w.WriteHeader(http.StatusNoContent)
3✔
714
}
715

716
func (i *inventoryHandlers) GetDevicesByGroupHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
717
        ctx := r.Context()
2✔
718

2✔
719
        l := log.FromContext(ctx)
2✔
720

2✔
721
        group := r.PathParam("name")
2✔
722

2✔
723
        page, perPage, err := utils.ParsePagination(r)
2✔
724
        if err != nil {
3✔
725
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
726
                return
1✔
727
        }
1✔
728

729
        //get one extra device to see if there's a 'next' page
730
        ids, totalCount, err := i.inventory.ListDevicesByGroup(
2✔
731
                ctx,
2✔
732
                model.GroupName(group),
2✔
733
                int((page-1)*perPage),
2✔
734
                int(perPage),
2✔
735
        )
2✔
736
        if err != nil {
4✔
737
                if err == store.ErrGroupNotFound {
4✔
738
                        u.RestErrWithLog(w, r, l, err, http.StatusNotFound)
2✔
739
                } else {
3✔
740
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
741
                }
1✔
742
                return
2✔
743
        }
744

745
        hasNext := totalCount > int(page*perPage)
2✔
746

2✔
747
        links := utils.MakePageLinkHdrs(r, page, perPage, hasNext)
2✔
748
        for _, l := range links {
4✔
749
                w.Header().Add("Link", l)
2✔
750
        }
2✔
751
        // the response writer will ensure the header name is in Kebab-Pascal-Case
752
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
753
        _ = w.WriteJson(ids)
2✔
754
}
755

756
func (i *inventoryHandlers) AppendDevicesToGroup(w rest.ResponseWriter, r *rest.Request) {
1✔
757
        var deviceIDs []model.DeviceID
1✔
758
        ctx := r.Context()
1✔
759
        l := log.FromContext(ctx)
1✔
760
        groupName := model.GroupName(r.PathParam("name"))
1✔
761
        if err := groupName.Validate(); err != nil {
2✔
762
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
763
                return
1✔
764
        }
1✔
765

766
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
2✔
767
                u.RestErrWithLog(w, r, l,
1✔
768
                        errors.Wrap(err, "invalid payload schema"),
1✔
769
                        http.StatusBadRequest,
1✔
770
                )
1✔
771
                return
1✔
772
        } else if len(deviceIDs) == 0 {
3✔
773
                u.RestErrWithLog(w, r, l,
1✔
774
                        errors.New("no device IDs present in payload"),
1✔
775
                        http.StatusBadRequest,
1✔
776
                )
1✔
777
                return
1✔
778
        }
1✔
779
        updated, err := i.inventory.UpdateDevicesGroup(
1✔
780
                ctx, deviceIDs, groupName,
1✔
781
        )
1✔
782
        if err != nil {
2✔
783
                u.RestErrWithLogInternal(w, r, l, err)
1✔
784
                return
1✔
785
        }
1✔
786
        _ = w.WriteJson(updated)
1✔
787
}
788

789
func (i *inventoryHandlers) DeleteGroupHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
790
        ctx := r.Context()
1✔
791
        l := log.FromContext(ctx)
1✔
792

1✔
793
        groupName := model.GroupName(r.PathParam("name"))
1✔
794
        if err := groupName.Validate(); err != nil {
2✔
795
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
796
                return
1✔
797
        }
1✔
798

799
        updated, err := i.inventory.DeleteGroup(ctx, groupName)
1✔
800
        if err != nil {
2✔
801
                u.RestErrWithLogInternal(w, r, l, err)
1✔
802
                return
1✔
803
        }
1✔
804
        w.WriteHeader(http.StatusOK)
1✔
805
        _ = w.WriteJson(updated)
1✔
806
}
807

808
func (i *inventoryHandlers) ClearDevicesGroupHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
809
        var deviceIDs []model.DeviceID
1✔
810
        ctx := r.Context()
1✔
811
        l := log.FromContext(ctx)
1✔
812

1✔
813
        groupName := model.GroupName(r.PathParam("name"))
1✔
814
        if err := groupName.Validate(); err != nil {
2✔
815
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
816
                return
1✔
817
        }
1✔
818

819
        if err := r.DecodeJsonPayload(&deviceIDs); err != nil {
2✔
820
                u.RestErrWithLog(w, r, l,
1✔
821
                        errors.Wrap(err, "invalid payload schema"),
1✔
822
                        http.StatusBadRequest,
1✔
823
                )
1✔
824
                return
1✔
825
        } else if len(deviceIDs) == 0 {
3✔
826
                u.RestErrWithLog(w, r, l,
1✔
827
                        errors.New("no device IDs present in payload"),
1✔
828
                        http.StatusBadRequest,
1✔
829
                )
1✔
830
                return
1✔
831
        }
1✔
832

833
        updated, err := i.inventory.UnsetDevicesGroup(ctx, deviceIDs, groupName)
1✔
834
        if err != nil {
2✔
835
                u.RestErrWithLogInternal(w, r, l, err)
1✔
836
                return
1✔
837
        }
1✔
838
        w.WriteHeader(http.StatusOK)
1✔
839
        _ = w.WriteJson(updated)
1✔
840
}
841

842
func parseDevice(r *rest.Request) (*model.Device, error) {
3✔
843
        dev := model.Device{}
3✔
844

3✔
845
        //decode body
3✔
846
        err := r.DecodeJsonPayload(&dev)
3✔
847
        if err != nil {
5✔
848
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
849
        }
2✔
850

851
        if err := dev.Validate(); err != nil {
4✔
852
                return nil, err
1✔
853
        }
1✔
854

855
        return &dev, nil
3✔
856
}
857

858
func parseAttributes(r *rest.Request) (model.DeviceAttributes, error) {
3✔
859
        var attrs model.DeviceAttributes
3✔
860

3✔
861
        err := r.DecodeJsonPayload(&attrs)
3✔
862
        if err != nil {
5✔
863
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
864
        }
2✔
865

866
        err = attrs.Validate()
3✔
867
        if err != nil {
5✔
868
                return nil, err
2✔
869
        }
2✔
870

871
        return attrs, nil
3✔
872
}
873

874
func (i *inventoryHandlers) GetGroupsHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
875
        var fltr []model.FilterPredicate
2✔
876
        ctx := r.Context()
2✔
877

2✔
878
        l := log.FromContext(ctx)
2✔
879

2✔
880
        query := r.URL.Query()
2✔
881
        status := query.Get("status")
2✔
882
        if status != "" {
3✔
883
                fltr = []model.FilterPredicate{{
1✔
884
                        Attribute: "status",
1✔
885
                        Scope:     "identity",
1✔
886
                        Type:      "$eq",
1✔
887
                        Value:     status,
1✔
888
                }}
1✔
889
        }
1✔
890

891
        groups, err := i.inventory.ListGroups(ctx, fltr)
2✔
892
        if err != nil {
3✔
893
                u.RestErrWithLogInternal(w, r, l, err)
1✔
894
                return
1✔
895
        }
1✔
896

897
        if groups == nil {
3✔
898
                groups = []model.GroupName{}
1✔
899
        }
1✔
900

901
        _ = w.WriteJson(groups)
2✔
902
}
903

904
func (i *inventoryHandlers) GetDeviceGroupHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
905
        ctx := r.Context()
1✔
906

1✔
907
        l := log.FromContext(ctx)
1✔
908

1✔
909
        deviceID := r.PathParam("id")
1✔
910

1✔
911
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
1✔
912
        if err != nil {
2✔
913
                if err == store.ErrDevNotFound {
2✔
914
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
1✔
915
                } else {
2✔
916
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
917
                }
1✔
918
                return
1✔
919
        }
920

921
        ret := map[string]*model.GroupName{"group": nil}
1✔
922

1✔
923
        if group != "" {
2✔
924
                ret["group"] = &group
1✔
925
        }
1✔
926

927
        _ = w.WriteJson(ret)
1✔
928
}
929

930
type newTenantRequest struct {
931
        TenantID string `json:"tenant_id" valid:"required"`
932
}
933

934
func (t newTenantRequest) Validate() error {
2✔
935
        return validation.ValidateStruct(&t,
2✔
936
                validation.Field(&t.TenantID, validation.Required),
2✔
937
        )
2✔
938
}
2✔
939

940
func (i *inventoryHandlers) CreateTenantHandler(w rest.ResponseWriter, r *rest.Request) {
3✔
941
        ctx := r.Context()
3✔
942

3✔
943
        l := log.FromContext(ctx)
3✔
944

3✔
945
        var newTenant newTenantRequest
3✔
946

3✔
947
        if err := r.DecodeJsonPayload(&newTenant); err != nil {
5✔
948
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
949
                return
2✔
950
        }
2✔
951

952
        if err := newTenant.Validate(); err != nil {
4✔
953
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
954
                return
2✔
955
        }
2✔
956

957
        err := i.inventory.CreateTenant(ctx, model.NewTenant{
2✔
958
                ID: newTenant.TenantID,
2✔
959
        })
2✔
960
        if err != nil {
3✔
961
                u.RestErrWithLogInternal(w, r, l, err)
1✔
962
                return
1✔
963
        }
1✔
964

965
        w.WriteHeader(http.StatusCreated)
2✔
966
}
967

968
func (i *inventoryHandlers) FiltersAttributesHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
969
        ctx := r.Context()
2✔
970

2✔
971
        l := log.FromContext(ctx)
2✔
972

2✔
973
        // query the database
2✔
974
        attributes, err := i.inventory.GetFiltersAttributes(ctx)
2✔
975
        if err != nil {
3✔
976
                u.RestErrWithLogInternal(w, r, l, err)
1✔
977
                return
1✔
978
        }
1✔
979

980
        // in case of nil make sure we return empty list
981
        if attributes == nil {
4✔
982
                attributes = []model.FilterAttribute{}
2✔
983
        }
2✔
984

985
        _ = w.WriteJson(attributes)
2✔
986
}
987

988
func (i *inventoryHandlers) FiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
989
        ctx := r.Context()
1✔
990

1✔
991
        l := log.FromContext(ctx)
1✔
992

1✔
993
        //extract attributes from body
1✔
994
        searchParams, err := parseSearchParams(r)
1✔
995
        if err != nil {
2✔
996
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
997
                return
1✔
998
        }
1✔
999

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

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

1016
func (i *inventoryHandlers) InternalFiltersSearchHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
1017
        ctx := r.Context()
2✔
1018

2✔
1019
        l := log.FromContext(ctx)
2✔
1020

2✔
1021
        tenantId := r.PathParam("tenant_id")
2✔
1022
        if tenantId != "" {
4✔
1023
                ctx = getTenantContext(ctx, tenantId)
2✔
1024
        }
2✔
1025

1026
        //extract attributes from body
1027
        searchParams, err := parseSearchParams(r)
2✔
1028
        if err != nil {
4✔
1029
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
2✔
1030
                return
2✔
1031
        }
2✔
1032

1033
        // query the database
1034
        devs, totalCount, err := i.inventory.SearchDevices(ctx, *searchParams)
2✔
1035
        if err != nil {
3✔
1036
                if strings.Contains(err.Error(), "BadValue") {
2✔
1037
                        u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1038
                } else {
2✔
1039
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1040
                }
1✔
1041
                return
1✔
1042
        }
1043

1044
        // the response writer will ensure the header name is in Kebab-Pascal-Case
1045
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
2✔
1046
        _ = w.WriteJson(devs)
2✔
1047
}
1048

1049
func getTenantContext(ctx context.Context, tenantId string) context.Context {
2✔
1050
        if ctx == nil {
2✔
1051
                ctx = context.Background()
×
1052
        }
×
1053
        if tenantId == "" {
4✔
1054
                return ctx
2✔
1055
        }
2✔
1056
        id := &midentity.Identity{
2✔
1057
                Tenant: tenantId,
2✔
1058
        }
2✔
1059

2✔
1060
        ctx = midentity.WithContext(ctx, id)
2✔
1061

2✔
1062
        return ctx
2✔
1063
}
1064

1065
func (i *inventoryHandlers) InternalDevicesStatusHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
1066
        const (
2✔
1067
                StatusDecommissioned = "decommissioned"
2✔
1068
                StatusAccepted       = "accepted"
2✔
1069
                StatusRejected       = "rejected"
2✔
1070
                StatusPreauthorized  = "preauthorized"
2✔
1071
                StatusPending        = "pending"
2✔
1072
                StatusNoAuth         = "noauth"
2✔
1073
        )
2✔
1074
        var (
2✔
1075
                devices []model.DeviceUpdate
2✔
1076
                result  *model.UpdateResult
2✔
1077
        )
2✔
1078

2✔
1079
        ctx := r.Context()
2✔
1080
        l := log.FromContext(ctx)
2✔
1081

2✔
1082
        tenantID := r.PathParam("tenant_id")
2✔
1083
        ctx = getTenantContext(ctx, tenantID)
2✔
1084

2✔
1085
        status := r.PathParam("status")
2✔
1086

2✔
1087
        err := r.DecodeJsonPayload(&devices)
2✔
1088
        if err != nil {
4✔
1089
                u.RestErrWithLog(w, r, l, errors.Wrap(err, "cant parse devices"), http.StatusBadRequest)
2✔
1090
                return
2✔
1091
        }
2✔
1092

1093
        switch status {
2✔
1094
        case StatusAccepted, StatusPreauthorized,
1095
                StatusPending, StatusRejected,
1096
                StatusNoAuth:
2✔
1097
                // Update statuses
2✔
1098
                attrs := model.DeviceAttributes{{
2✔
1099
                        Name:  "status",
2✔
1100
                        Scope: model.AttrScopeIdentity,
2✔
1101
                        Value: status,
2✔
1102
                }}
2✔
1103
                result, err = i.inventory.UpsertDevicesStatuses(ctx, devices, attrs)
2✔
1104
        case StatusDecommissioned:
1✔
1105
                // Delete Inventory
1✔
1106
                result, err = i.inventory.DeleteDevices(ctx, getIdsFromDevices(devices))
1✔
1107
        default:
1✔
1108
                // Unrecognized status
1✔
1109
                u.RestErrWithLog(w, r, l,
1✔
1110
                        errors.Errorf("unrecognized status: %s", status),
1✔
1111
                        http.StatusNotFound,
1✔
1112
                )
1✔
1113
                return
1✔
1114
        }
1115
        if err == store.ErrWriteConflict {
3✔
1116
                u.RestErrWithLog(w, r, l, err, http.StatusConflict)
1✔
1117
                return
1✔
1118
        } else if err != nil {
4✔
1119
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1120
                return
1✔
1121
        }
1✔
1122

1123
        w.WriteHeader(http.StatusOK)
2✔
1124
        _ = w.WriteJson(result)
2✔
1125
}
1126

1127
func (i *inventoryHandlers) GetDeviceGroupsInternalHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
1128
        ctx := r.Context()
2✔
1129

2✔
1130
        l := log.FromContext(ctx)
2✔
1131

2✔
1132
        tenantId := r.PathParam("tenant_id")
2✔
1133
        ctx = getTenantContext(ctx, tenantId)
2✔
1134

2✔
1135
        deviceID := r.PathParam("device_id")
2✔
1136
        group, err := i.inventory.GetDeviceGroup(ctx, model.DeviceID(deviceID))
2✔
1137
        if err != nil {
4✔
1138
                if err == store.ErrDevNotFound {
4✔
1139
                        u.RestErrWithLog(w, r, l, store.ErrDevNotFound, http.StatusNotFound)
2✔
1140
                } else {
3✔
1141
                        u.RestErrWithLogInternal(w, r, l, err)
1✔
1142
                }
1✔
1143
                return
2✔
1144
        }
1145

1146
        res := model.DeviceGroups{}
2✔
1147
        if group != "" {
3✔
1148
                res.Groups = append(res.Groups, string(group))
1✔
1149
        }
1✔
1150

1151
        _ = w.WriteJson(res)
2✔
1152
}
1153

1154
func (i *inventoryHandlers) ReindexDeviceDataHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
1155
        ctx := r.Context()
2✔
1156
        tenantId := r.PathParam("tenant_id")
2✔
1157
        ctx = getTenantContext(ctx, tenantId)
2✔
1158

2✔
1159
        l := log.FromContext(ctx)
2✔
1160

2✔
1161
        deviceId := r.PathParam("device_id")
2✔
1162
        if len(deviceId) < 1 {
3✔
1163
                u.RestErrWithLog(w, r, l, errors.New("device id cannot be empty"), http.StatusBadRequest)
1✔
1164
                return
1✔
1165
        }
1✔
1166

1167
        serviceName, err := utils.ParseQueryParmStr(r, "service", false, nil)
2✔
1168
        // inventory service accepts only reindex requests from devicemonitor
2✔
1169
        if err != nil || serviceName != "devicemonitor" {
4✔
1170
                u.RestErrWithLog(w, r, l, errors.New("unsupported service"), http.StatusBadRequest)
2✔
1171
                return
2✔
1172
        }
2✔
1173

1174
        // check devicemonitor alerts
1175
        alertsCount, err := i.inventory.CheckAlerts(ctx, deviceId)
1✔
1176
        if err != nil {
2✔
1177
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1178
                return
1✔
1179
        }
1✔
1180

1181
        alertsPresent := false
1✔
1182
        if alertsCount > 0 {
2✔
1183
                alertsPresent = true
1✔
1184
        }
1✔
1185
        attrs := model.DeviceAttributes{
1✔
1186
                model.DeviceAttribute{
1✔
1187
                        Name:  model.AttrNameNumberOfAlerts,
1✔
1188
                        Scope: model.AttrScopeMonitor,
1✔
1189
                        Value: alertsCount,
1✔
1190
                },
1✔
1191
                model.DeviceAttribute{
1✔
1192
                        Name:  model.AttrNameAlerts,
1✔
1193
                        Scope: model.AttrScopeMonitor,
1✔
1194
                        Value: alertsPresent,
1✔
1195
                },
1✔
1196
        }
1✔
1197

1✔
1198
        // upsert monitor attributes
1✔
1199
        err = i.inventory.UpsertAttributes(ctx, model.DeviceID(deviceId), attrs)
1✔
1200
        cause := errors.Cause(err)
1✔
1201
        switch cause {
1✔
1202
        case store.ErrNoAttrName:
×
1203
                u.RestErrWithLog(w, r, l, cause, http.StatusBadRequest)
×
1204
                return
×
1205
        }
1206
        if err != nil {
2✔
1207
                u.RestErrWithLogInternal(w, r, l, err)
1✔
1208
                return
1✔
1209
        }
1✔
1210

1211
        w.WriteHeader(http.StatusOK)
1✔
1212
}
1213

1214
func getIdsFromDevices(devices []model.DeviceUpdate) []model.DeviceID {
1✔
1215
        ids := make([]model.DeviceID, len(devices))
1✔
1216
        for i, dev := range devices {
2✔
1217
                ids[i] = dev.Id
1✔
1218
        }
1✔
1219
        return ids
1✔
1220
}
1221

1222
func parseSearchParams(r *rest.Request) (*model.SearchParams, error) {
2✔
1223
        var searchParams model.SearchParams
2✔
1224

2✔
1225
        if err := r.DecodeJsonPayload(&searchParams); err != nil {
4✔
1226
                return nil, errors.Wrap(err, "failed to decode request body")
2✔
1227
        }
2✔
1228

1229
        if searchParams.Page < 1 {
3✔
1230
                searchParams.Page = utils.PageDefault
1✔
1231
        }
1✔
1232
        if searchParams.PerPage < 1 {
3✔
1233
                searchParams.PerPage = utils.PerPageDefault
1✔
1234
        }
1✔
1235

1236
        if err := searchParams.Validate(); err != nil {
3✔
1237
                return nil, err
1✔
1238
        }
1✔
1239

1240
        return &searchParams, nil
2✔
1241
}
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