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

mendersoftware / mender-server / 1622978334

13 Jan 2025 03:51PM UTC coverage: 72.802% (-3.8%) from 76.608%
1622978334

Pull #300

gitlab-ci

alfrunes
fix: Deployment device count should not exceed max devices

Added a condition to skip deployments when the device count reaches max
devices.

Changelog: Title
Ticket: MEN-7847
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #300: fix: Deployment device count should not exceed max devices

4251 of 6164 branches covered (68.96%)

Branch coverage included in aggregate %.

0 of 18 new or added lines in 1 file covered. (0.0%)

2544 existing lines in 83 files now uncovered.

42741 of 58384 relevant lines covered (73.21%)

21.49 hits per line

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

93.8
/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
        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
        checkInTimeParamScope = "system"
95
)
96

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

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

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

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

117
func wrapRoutes(middleware rest.Middleware, routes ...*rest.Route) []*rest.Route {
1✔
118
        for _, route := range routes {
2✔
119
                route.Func = middleware.MiddlewareFunc(route.Func)
1✔
120
        }
1✔
121
        return routes
1✔
122
}
123

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

1✔
131
        api := rest.NewApi()
1✔
132

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

1✔
154
                rest.Patch(urlInternalAttributes, i.PatchDeviceAttributesInternalHandler),
1✔
155
                rest.Post(urlInternalReindex, i.ReindexDeviceDataHandler),
1✔
156

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

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

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

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

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

1✔
197
        return api.MakeHandler(), nil
1✔
198

199
}
200

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

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

1✔
209
        ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
1✔
210
        defer cancel()
1✔
211

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

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

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

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

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

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

314
                        if filter.Value == "" {
2✔
315
                                filter.Value = valueStr
1✔
316
                                filter.Operator = store.Eq
1✔
317
                        }
1✔
318
                }
319

320
                floatValue, err := strconv.ParseFloat(filter.Value, 64)
1✔
321
                if err == nil {
2✔
322
                        filter.ValueFloat = &floatValue
1✔
323
                }
1✔
324

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

330
                filters = append(filters, filter)
1✔
331
        }
332
        return filters, nil
1✔
333
}
334

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

1✔
338
        l := log.FromContext(ctx)
1✔
339

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

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

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

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

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

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

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

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

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

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

1✔
397
        l := log.FromContext(ctx)
1✔
398

1✔
399
        deviceID := r.PathParam("id")
1✔
400

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

414
        _ = w.WriteJson(dev)
1✔
415
}
416

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

1✔
420
        l := log.FromContext(ctx)
1✔
421

1✔
422
        deviceID := r.PathParam("id")
1✔
423

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

431
        w.WriteHeader(http.StatusNoContent)
1✔
432
}
433

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

444
        l := log.FromContext(ctx)
1✔
445

1✔
446
        deviceID := r.PathParam("device_id")
1✔
447

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

454
        w.WriteHeader(http.StatusNoContent)
1✔
455
}
456

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

467
        l := log.FromContext(ctx)
1✔
468

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

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

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

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

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

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

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

520
        ifMatchHeader := r.Header.Get("If-Match")
1✔
521

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

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

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

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

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

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

578
        w.WriteHeader(http.StatusOK)
1✔
579
}
580

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

1✔
589
        l := log.FromContext(ctx)
1✔
590

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

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

627
        w.WriteHeader(http.StatusOK)
1✔
628
}
629

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

1✔
633
        l := log.FromContext(ctx)
1✔
634

1✔
635
        deviceID := r.PathParam("id")
1✔
636
        groupName := r.PathParam("name")
1✔
637

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

651
        w.WriteHeader(http.StatusNoContent)
1✔
652
}
653

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

1✔
657
        l := log.FromContext(ctx)
1✔
658

1✔
659
        devId := r.PathParam("id")
1✔
660

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

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

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

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

1✔
690
        l := log.FromContext(ctx)
1✔
691

1✔
692
        group := r.PathParam("name")
1✔
693

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

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

716
        hasNext := totalCount > int(page*perPage)
1✔
717

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

727
func (i *inventoryHandlers) AppendDevicesToGroup(w rest.ResponseWriter, r *rest.Request) {
1✔
728
        var deviceIDs []model.DeviceID
1✔
729
        ctx := r.Context()
1✔
730
        l := log.FromContext(ctx)
1✔
731
        groupName := model.GroupName(r.PathParam("name"))
1✔
732
        if err := groupName.Validate(); err != nil {
2✔
733
                u.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
734
                return
1✔
735
        }
1✔
736

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

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

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

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

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

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

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

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

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

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

822
        if err := dev.Validate(); err != nil {
2✔
823
                return nil, err
1✔
824
        }
1✔
825

826
        return &dev, nil
1✔
827
}
828

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

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

837
        err = attrs.Validate()
1✔
838
        if err != nil {
2✔
839
                return nil, err
1✔
840
        }
1✔
841

842
        return attrs, nil
1✔
843
}
844

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

1✔
849
        l := log.FromContext(ctx)
1✔
850

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

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

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

872
        _ = w.WriteJson(groups)
1✔
873
}
874

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

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

1✔
880
        deviceID := r.PathParam("id")
1✔
881

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

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

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

898
        _ = w.WriteJson(ret)
1✔
899
}
900

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

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

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

1✔
914
        l := log.FromContext(ctx)
1✔
915

1✔
916
        var newTenant newTenantRequest
1✔
917

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

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

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

936
        w.WriteHeader(http.StatusCreated)
1✔
937
}
938

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

1✔
942
        l := log.FromContext(ctx)
1✔
943

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

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

956
        _ = w.WriteJson(attributes)
1✔
957
}
958

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

1✔
962
        l := log.FromContext(ctx)
1✔
963

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

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

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

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

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

1✔
992
        tenantId := r.PathParam("tenant_id")
1✔
993
        if tenantId != "" {
2✔
994
                ctx = getTenantContext(ctx, tenantId)
1✔
995
        }
1✔
996

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

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

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

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

1✔
1031
        ctx = midentity.WithContext(ctx, id)
1✔
1032

1✔
1033
        return ctx
1✔
1034
}
1035

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

1✔
1050
        ctx := r.Context()
1✔
1051
        l := log.FromContext(ctx)
1✔
1052

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

1✔
1056
        status := r.PathParam("status")
1✔
1057

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

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

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

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

1✔
1101
        l := log.FromContext(ctx)
1✔
1102

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

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

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

1122
        _ = w.WriteJson(res)
1✔
1123
}
1124

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

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

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

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

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

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

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

1182
        w.WriteHeader(http.StatusOK)
1✔
1183
}
1184

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

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

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

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

1207
        if err := searchParams.Validate(); err != nil {
2✔
1208
                return nil, err
1✔
1209
        }
1✔
1210

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