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

mendersoftware / reporting / 732508278

pending completion
732508278

Pull #87

gitlab-ci

Fabio Tranchitella
refac: rename internal search end-point to `/tenants/{tenant_id}/devices/search`
Pull Request #87: MEN-5930: index device deployment objects

331 of 402 new or added lines in 12 files covered. (82.34%)

135 existing lines in 5 files now uncovered.

2196 of 2600 relevant lines covered (84.46%)

17.72 hits per line

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

77.26
/app/reporting/reporting.go
1
// Copyright 2022 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 reporting
16

17
import (
18
        "context"
19
        "errors"
20
        "sort"
21
        "time"
22

23
        "github.com/mendersoftware/go-lib-micro/log"
24

25
        "github.com/mendersoftware/reporting/client/inventory"
26
        "github.com/mendersoftware/reporting/mapping"
27
        "github.com/mendersoftware/reporting/model"
28
        "github.com/mendersoftware/reporting/store"
29
)
30

31
//go:generate ../../x/mockgen.sh
32
type App interface {
33
        HealthCheck(ctx context.Context) error
34
        GetMapping(ctx context.Context, tid string) (*model.Mapping, error)
35
        GetSearchableInvAttrs(ctx context.Context, tid string) ([]model.FilterAttribute, error)
36
        AggregateDevices(ctx context.Context, aggregateParams *model.AggregateParams) (
37
                []model.DeviceAggregation, error)
38
        SearchDevices(ctx context.Context, searchParams *model.SearchParams) (
39
                []inventory.Device, int, error)
40
}
41

42
type app struct {
43
        store  store.Store
44
        mapper mapping.Mapper
45
        ds     store.DataStore
46
}
47

48
func NewApp(store store.Store, ds store.DataStore) App {
29✔
49
        mapper := mapping.NewMapper(ds)
29✔
50
        return &app{
29✔
51
                store:  store,
29✔
52
                mapper: mapper,
29✔
53
                ds:     ds,
29✔
54
        }
29✔
55
}
29✔
56

57
// HealthCheck performs a health check and returns an error if it fails
58
func (a *app) HealthCheck(ctx context.Context) error {
6✔
59
        err := a.ds.Ping(ctx)
6✔
60
        if err == nil {
10✔
61
                err = a.store.Ping(ctx)
4✔
62
        }
4✔
63
        return err
6✔
64
}
65

66
// GetMapping returns the mapping for the specified tenant
67
func (app *app) GetMapping(ctx context.Context, tid string) (*model.Mapping, error) {
2✔
68
        return app.ds.GetMapping(ctx, tid)
2✔
69
}
2✔
70

71
// AggregateDevices aggregates device data
72
func (app *app) AggregateDevices(
73
        ctx context.Context,
74
        aggregateParams *model.AggregateParams,
75
) ([]model.DeviceAggregation, error) {
6✔
76
        searchParams := &model.SearchParams{
6✔
77
                Filters:  aggregateParams.Filters,
6✔
78
                Groups:   aggregateParams.Groups,
6✔
79
                TenantID: aggregateParams.TenantID,
6✔
80
        }
6✔
81
        if err := app.mapSearchParams(ctx, searchParams); err != nil {
6✔
UNCOV
82
                return nil, err
×
UNCOV
83
        }
×
84
        query, err := model.BuildQuery(*searchParams)
6✔
85
        if err != nil {
6✔
86
                return nil, err
×
87
        }
×
88
        if searchParams.TenantID != "" {
12✔
89
                query = query.Must(model.M{
6✔
90
                        "term": model.M{
6✔
91
                                model.FieldNameTenantID: searchParams.TenantID,
6✔
92
                        },
6✔
93
                })
6✔
94
        }
6✔
95

96
        if err := app.mapAggregations(ctx, searchParams.TenantID,
6✔
97
                aggregateParams.Aggregations); err != nil {
6✔
UNCOV
98
                return nil, err
×
UNCOV
99
        }
×
100
        aggregations, err := model.BuildAggregations(aggregateParams.Aggregations)
6✔
101
        if err != nil {
6✔
102
                return nil, err
×
103
        }
×
104

105
        query = query.WithSize(0).With(map[string]interface{}{
6✔
106
                "aggs": aggregations,
6✔
107
        })
6✔
108
        esRes, err := app.store.AggregateDevices(ctx, query)
6✔
109
        if err != nil {
6✔
UNCOV
110
                return nil, err
×
UNCOV
111
        }
×
112

113
        aggregationsS, ok := esRes["aggregations"].(map[string]interface{})
6✔
114
        if !ok {
6✔
115
                return nil, errors.New("can't process store aggregations slice")
×
UNCOV
116
        }
×
117
        res, err := app.storeToDeviceAggregations(ctx, searchParams.TenantID, aggregationsS)
6✔
118
        if err != nil {
6✔
119
                return nil, err
×
120
        }
×
121

122
        return res, nil
6✔
123
}
124

125
// storeToDeviceAggregations translates ES results directly to device aggregations
126
func (a *app) storeToDeviceAggregations(
127
        ctx context.Context, tenantID string, aggregationsS map[string]interface{},
128
) ([]model.DeviceAggregation, error) {
21✔
129
        aggs := []model.DeviceAggregation{}
21✔
130
        for name, aggregationS := range aggregationsS {
61✔
131
                if _, ok := aggregationS.(map[string]interface{}); !ok {
70✔
132
                        continue
30✔
133
                }
134
                bucketsS, ok := aggregationS.(map[string]interface{})["buckets"].([]interface{})
10✔
135
                if !ok {
10✔
UNCOV
136
                        continue
×
137
                }
138
                items := make([]model.DeviceAggregationItem, 0, len(bucketsS))
10✔
139
                for _, bucket := range bucketsS {
25✔
140
                        bucketMap, ok := bucket.(map[string]interface{})
15✔
141
                        if !ok {
15✔
UNCOV
142
                                return nil, errors.New("can't process store bucket item")
×
UNCOV
143
                        }
×
144
                        key, ok := bucketMap["key"].(string)
15✔
145
                        if !ok {
15✔
146
                                return nil, errors.New("can't process store key attribute")
×
147
                        }
×
148
                        count, ok := bucketMap["doc_count"].(float64)
15✔
149
                        if !ok {
15✔
150
                                return nil, errors.New("can't process store doc_count attribute")
×
151
                        }
×
152
                        item := model.DeviceAggregationItem{
15✔
153
                                Key:   key,
15✔
154
                                Count: int(count),
15✔
155
                        }
15✔
156
                        subaggs, err := a.storeToDeviceAggregations(ctx, tenantID, bucketMap)
15✔
157
                        if err == nil && len(subaggs) > 0 {
19✔
158
                                item.Aggregations = subaggs
4✔
159
                        }
4✔
160
                        items = append(items, item)
15✔
161
                }
162

163
                otherCount := 0
10✔
164
                if count, ok := aggregationS.(map[string]interface{})["sum_other_doc_count"].(float64); ok {
20✔
165
                        otherCount = int(count)
10✔
166
                }
10✔
167

168
                aggs = append(aggs, model.DeviceAggregation{
10✔
169
                        Name:       name,
10✔
170
                        Items:      items,
10✔
171
                        OtherCount: otherCount,
10✔
172
                })
10✔
173
        }
174
        return aggs, nil
21✔
175
}
176

177
// SearchDevices searches device data
178
func (app *app) SearchDevices(
179
        ctx context.Context,
180
        searchParams *model.SearchParams,
181
) ([]inventory.Device, int, error) {
29✔
182
        if err := app.mapSearchParams(ctx, searchParams); err != nil {
29✔
UNCOV
183
                return nil, 0, err
×
UNCOV
184
        }
×
185
        query, err := model.BuildQuery(*searchParams)
29✔
186
        if err != nil {
31✔
187
                return nil, 0, err
2✔
188
        }
2✔
189

190
        if searchParams.TenantID != "" {
44✔
191
                query = query.Must(model.M{
17✔
192
                        "term": model.M{
17✔
193
                                model.FieldNameTenantID: searchParams.TenantID,
17✔
194
                        },
17✔
195
                })
17✔
196
        }
17✔
197

198
        if len(searchParams.DeviceIDs) > 0 {
31✔
199
                query = query.Must(model.M{
4✔
200
                        "terms": model.M{
4✔
201
                                model.FieldNameID: searchParams.DeviceIDs,
4✔
202
                        },
4✔
203
                })
4✔
204
        }
4✔
205

206
        esRes, err := app.store.SearchDevices(ctx, query)
27✔
207
        if err != nil {
29✔
208
                return nil, 0, err
2✔
209
        }
2✔
210

211
        res, total, err := app.storeToInventoryDevs(ctx, searchParams.TenantID, esRes)
25✔
212
        if err != nil {
27✔
213
                return nil, 0, err
2✔
214
        }
2✔
215

216
        return res, total, err
23✔
217
}
218

219
func (app *app) mapAggregations(ctx context.Context, tenantID string,
220
        aggregations []model.AggregationTerm) error {
8✔
221
        attributes := make(inventory.DeviceAttributes, 0, len(aggregations))
8✔
222
        for i := range aggregations {
16✔
223
                attributes = append(attributes, inventory.DeviceAttribute{
8✔
224
                        Name:  aggregations[i].Attribute,
8✔
225
                        Scope: aggregations[i].Scope,
8✔
226
                })
8✔
227
        }
8✔
228
        attributes, err := app.mapper.MapInventoryAttributes(ctx, tenantID,
8✔
229
                attributes, false, true)
8✔
230
        if err == nil {
16✔
231
                for i, attr := range attributes {
16✔
232
                        aggregations[i].Attribute = attr.Name
8✔
233
                        aggregations[i].Scope = attr.Scope
8✔
234
                        if len(aggregations[i].Aggregations) > 0 {
10✔
235
                                err = app.mapAggregations(ctx, tenantID, aggregations[i].Aggregations)
2✔
236
                                if err != nil {
2✔
UNCOV
237
                                        break
×
238
                                }
239
                        }
240
                }
241
        }
242
        return err
8✔
243
}
244

245
func (app *app) mapSearchParams(ctx context.Context, searchParams *model.SearchParams) error {
35✔
246
        if len(searchParams.Filters) > 0 {
63✔
247
                attributes := make(inventory.DeviceAttributes, 0, len(searchParams.Attributes))
28✔
248
                for i := 0; i < len(searchParams.Filters); i++ {
56✔
249
                        attributes = append(attributes, inventory.DeviceAttribute{
28✔
250
                                Name:        searchParams.Filters[i].Attribute,
28✔
251
                                Scope:       searchParams.Filters[i].Scope,
28✔
252
                                Value:       searchParams.Filters[i].Value,
28✔
253
                                Description: &searchParams.Filters[i].Type,
28✔
254
                        })
28✔
255
                }
28✔
256
                attributes, err := app.mapper.MapInventoryAttributes(ctx, searchParams.TenantID,
28✔
257
                        attributes, false, true)
28✔
258
                if err != nil {
28✔
UNCOV
259
                        return err
×
UNCOV
260
                }
×
261
                searchParams.Filters = make([]model.FilterPredicate, 0, len(searchParams.Filters))
28✔
262
                for _, attribute := range attributes {
56✔
263
                        searchParams.Filters = append(searchParams.Filters, model.FilterPredicate{
28✔
264
                                Attribute: attribute.Name,
28✔
265
                                Scope:     attribute.Scope,
28✔
266
                                Value:     attribute.Value,
28✔
267
                                Type:      *attribute.Description,
28✔
268
                        })
28✔
269
                }
28✔
270
        }
271
        if len(searchParams.Attributes) > 0 {
37✔
272
                attributes := make(inventory.DeviceAttributes, 0, len(searchParams.Attributes))
2✔
273
                for i := 0; i < len(searchParams.Attributes); i++ {
4✔
274
                        attributes = append(attributes, inventory.DeviceAttribute{
2✔
275
                                Name:  searchParams.Attributes[i].Attribute,
2✔
276
                                Scope: searchParams.Attributes[i].Scope,
2✔
277
                        })
2✔
278
                }
2✔
279
                attributes, err := app.mapper.MapInventoryAttributes(ctx, searchParams.TenantID,
2✔
280
                        attributes, false, false)
2✔
281
                if err != nil {
2✔
UNCOV
282
                        return err
×
UNCOV
283
                }
×
284
                searchParams.Attributes = make([]model.SelectAttribute, 0, len(searchParams.Attributes))
2✔
285
                for _, attribute := range attributes {
4✔
286
                        searchParams.Attributes = append(searchParams.Attributes, model.SelectAttribute{
2✔
287
                                Attribute: attribute.Name,
2✔
288
                                Scope:     attribute.Scope,
2✔
289
                        })
2✔
290
                }
2✔
291
        }
292
        if len(searchParams.Sort) > 0 {
50✔
293
                attributes := make(inventory.DeviceAttributes, 0, len(searchParams.Sort))
15✔
294
                for i := 0; i < len(searchParams.Sort); i++ {
30✔
295
                        attributes = append(attributes, inventory.DeviceAttribute{
15✔
296
                                Name:        searchParams.Sort[i].Attribute,
15✔
297
                                Scope:       searchParams.Sort[i].Scope,
15✔
298
                                Description: &searchParams.Sort[i].Order,
15✔
299
                        })
15✔
300
                }
15✔
301
                attributes, err := app.mapper.MapInventoryAttributes(ctx, searchParams.TenantID,
15✔
302
                        attributes, false, false)
15✔
303
                if err != nil {
15✔
UNCOV
304
                        return err
×
UNCOV
305
                }
×
306
                searchParams.Sort = make([]model.SortCriteria, 0, len(searchParams.Attributes))
15✔
307
                for _, attribute := range attributes {
30✔
308
                        searchParams.Sort = append(searchParams.Sort, model.SortCriteria{
15✔
309
                                Attribute: attribute.Name,
15✔
310
                                Scope:     attribute.Scope,
15✔
311
                                Order:     *attribute.Description,
15✔
312
                        })
15✔
313
                }
15✔
314
        }
315

316
        return nil
35✔
317
}
318

319
// storeToInventoryDevs translates ES results directly to inventory devices
320
func (a *app) storeToInventoryDevs(
321
        ctx context.Context, tenantID string, storeRes map[string]interface{},
322
) ([]inventory.Device, int, error) {
25✔
323
        devs := []inventory.Device{}
25✔
324

25✔
325
        hitsM, ok := storeRes["hits"].(map[string]interface{})
25✔
326
        if !ok {
25✔
UNCOV
327
                return nil, 0, errors.New("can't process store hits map")
×
UNCOV
328
        }
×
329

330
        hitsTotalM, ok := hitsM["total"].(map[string]interface{})
25✔
331
        if !ok {
25✔
332
                return nil, 0, errors.New("can't process total hits struct")
×
UNCOV
333
        }
×
334

335
        total, ok := hitsTotalM["value"].(float64)
25✔
336
        if !ok {
27✔
337
                return nil, 0, errors.New("can't process total hits value")
2✔
338
        }
2✔
339

340
        hitsS, ok := hitsM["hits"].([]interface{})
23✔
341
        if !ok {
23✔
UNCOV
342
                return nil, 0, errors.New("can't process store hits slice")
×
UNCOV
343
        }
×
344

345
        for _, v := range hitsS {
56✔
346
                res, err := a.storeToInventoryDev(ctx, tenantID, v)
33✔
347
                if err != nil {
33✔
UNCOV
348
                        return nil, 0, err
×
UNCOV
349
                }
×
350

351
                devs = append(devs, *res)
33✔
352
        }
353

354
        return devs, int(total), nil
23✔
355
}
356

357
func (a *app) storeToInventoryDev(ctx context.Context, tenantID string,
358
        storeRes interface{}) (*inventory.Device, error) {
33✔
359
        resM, ok := storeRes.(map[string]interface{})
33✔
360
        if !ok {
33✔
UNCOV
361
                return nil, errors.New("can't process individual hit")
×
UNCOV
362
        }
×
363

364
        // if query has a 'fields' clause, use 'fields' instead of '_source'
365
        sourceM, ok := resM["_source"].(map[string]interface{})
33✔
366
        if !ok {
33✔
UNCOV
367
                sourceM, ok = resM["fields"].(map[string]interface{})
×
UNCOV
368
                if !ok {
×
UNCOV
369
                        return nil, errors.New("can't process hit's '_source' nor 'fields'")
×
UNCOV
370
                }
×
371
        }
372

373
        // if query has a 'fields' clause, all results will be arrays incl. device id, so extract it
374
        id, ok := sourceM["id"].(string)
33✔
375
        if !ok {
33✔
UNCOV
376
                idarr, ok := sourceM["id"].([]interface{})
×
UNCOV
377
                if !ok {
×
UNCOV
378
                        return nil, errors.New(
×
UNCOV
379
                                "can't parse device id as neither single value nor array",
×
380
                        )
×
381
                }
×
382

383
                id, ok = idarr[0].(string)
×
384
                if !ok {
×
385
                        return nil, errors.New(
×
UNCOV
386
                                "can't parse device id as neither single value nor array",
×
387
                        )
×
388
                }
×
389
        }
390

391
        ret := &inventory.Device{
33✔
392
                ID: inventory.DeviceID(id),
33✔
393
        }
33✔
394

33✔
395
        attrs := []inventory.DeviceAttribute{}
33✔
396

33✔
397
        for k, v := range sourceM {
213✔
398
                s, n, err := model.MaybeParseAttr(k)
180✔
399
                if err != nil {
180✔
UNCOV
400
                        return nil, err
×
UNCOV
401
                }
×
402

403
                if vArray, ok := v.([]interface{}); ok && len(vArray) == 1 {
290✔
404
                        v = vArray[0]
110✔
405
                }
110✔
406

407
                if n != "" {
294✔
408
                        a := inventory.DeviceAttribute{
114✔
409
                                Name:  model.Redot(n),
114✔
410
                                Scope: s,
114✔
411
                                Value: v,
114✔
412
                        }
114✔
413

114✔
414
                        if a.Scope == model.ScopeSystem &&
114✔
415
                                a.Name == model.AttrNameUpdatedAt {
114✔
UNCOV
416
                                ret.UpdatedTs = parseTime(v)
×
417
                        } else if a.Scope == model.ScopeSystem &&
114✔
418
                                a.Name == model.AttrNameCreatedAt {
114✔
UNCOV
419
                                ret.CreatedTs = parseTime(v)
×
420
                        }
×
421

422
                        attrs = append(attrs, a)
114✔
423
                }
424
        }
425

426
        attributes, err := a.mapper.ReverseInventoryAttributes(ctx, tenantID, attrs)
33✔
427
        if err != nil {
33✔
UNCOV
428
                return nil, err
×
UNCOV
429
        }
×
430
        ret.Attributes = attributes
33✔
431

33✔
432
        return ret, nil
33✔
433
}
434

UNCOV
435
func parseTime(v interface{}) time.Time {
×
UNCOV
436
        val, _ := v.(string)
×
UNCOV
437
        if t, err := time.Parse(time.RFC3339, val); err == nil {
×
UNCOV
438
                return t
×
439
        }
×
440
        return time.Time{}
×
441
}
442

443
func (app *app) GetSearchableInvAttrs(
444
        ctx context.Context,
445
        tid string,
446
) ([]model.FilterAttribute, error) {
4✔
447
        l := log.FromContext(ctx)
4✔
448

4✔
449
        index, err := app.store.GetDevicesIndexMapping(ctx, tid)
4✔
450
        if err != nil {
6✔
451
                return nil, err
2✔
452
        }
2✔
453

454
        // inventory attributes are under 'mappings.properties'
455
        mappings, ok := index["mappings"]
2✔
456
        if !ok {
2✔
UNCOV
457
                return nil, errors.New("can't parse index mappings")
×
UNCOV
458
        }
×
459

460
        mappingsM, ok := mappings.(map[string]interface{})
2✔
461
        if !ok {
2✔
462
                return nil, errors.New("can't parse index mappings")
×
UNCOV
463
        }
×
464

465
        props, ok := mappingsM["properties"]
2✔
466
        if !ok {
2✔
467
                return nil, errors.New("can't parse index properties")
×
UNCOV
468
        }
×
469

470
        propsM, ok := props.(map[string]interface{})
2✔
471
        if !ok {
2✔
472
                return nil, errors.New("can't parse index properties")
×
UNCOV
473
        }
×
474

475
        attrs := []inventory.DeviceAttribute{}
2✔
476
        for k := range propsM {
8✔
477
                s, n, err := model.MaybeParseAttr(k)
6✔
478

6✔
479
                if err != nil {
6✔
UNCOV
480
                        return nil, err
×
UNCOV
481
                }
×
482

483
                if n != "" {
12✔
484
                        attrs = append(attrs, inventory.DeviceAttribute{Name: n, Scope: s})
6✔
485
                }
6✔
486
        }
487
        attributes, err := app.mapper.ReverseInventoryAttributes(ctx, tid, attrs)
2✔
488
        if err != nil {
2✔
UNCOV
489
                return nil, err
×
UNCOV
490
        }
×
491

492
        ret := []model.FilterAttribute{}
2✔
493
        for _, attr := range attributes {
8✔
494
                ret = append(ret, model.FilterAttribute{Name: attr.Name, Scope: attr.Scope, Count: 1})
6✔
495
        }
6✔
496

497
        sort.Slice(ret, func(i, j int) bool {
6✔
498
                if ret[j].Scope > ret[i].Scope {
4✔
UNCOV
499
                        return true
×
UNCOV
500
                }
×
501

502
                if ret[j].Scope < ret[i].Scope {
6✔
503
                        return false
2✔
504
                }
2✔
505

506
                return ret[j].Name > ret[i].Name
2✔
507
        })
508

509
        l.Debugf("parsed searchable attributes %v\n", ret)
2✔
510

2✔
511
        return ret, nil
2✔
512
}
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