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

mendersoftware / reporting / 715476761

pending completion
715476761

Pull #79

gitlab-ci

Fabio Tranchitella
feat: end-point to list the filterable attributes usage and limits
Pull Request #79: MEN-5598: map inventory attributes to sequential fields

441 of 510 new or added lines in 14 files covered. (86.47%)

13 existing lines in 2 files now uncovered.

1832 of 2225 relevant lines covered (82.34%)

15.09 hits per line

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

/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
        InventorySearchDevices(ctx context.Context, searchParams *model.SearchParams) (
37
                []inventory.Device, int, error)
38
}
39

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

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

55
// HealthCheck performs a health check and returns an error if it fails
NEW
56
func (a *app) HealthCheck(ctx context.Context) error {
×
NEW
57
        err := a.ds.Ping(ctx)
×
NEW
58
        if err == nil {
×
NEW
59
                err = a.store.Ping(ctx)
×
NEW
60
        }
×
NEW
61
        return err
×
62
}
63

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

69
func (app *app) InventorySearchDevices(
70
        ctx context.Context,
71
        searchParams *model.SearchParams,
72
) ([]inventory.Device, int, error) {
25✔
73
        if err := app.mapSearchParams(ctx, searchParams); err != nil {
25✔
NEW
74
                return nil, 0, err
×
NEW
75
        }
×
76
        query, err := model.BuildQuery(*searchParams)
25✔
77
        if err != nil {
27✔
78
                return nil, 0, err
2✔
79
        }
2✔
80

81
        if searchParams.TenantID != "" {
38✔
82
                query = query.Must(model.M{
15✔
83
                        "term": model.M{
15✔
84
                                model.FieldNameTenantID: searchParams.TenantID,
15✔
85
                        },
15✔
86
                })
15✔
87
        }
15✔
88

89
        if len(searchParams.DeviceIDs) > 0 {
25✔
90
                query = query.Must(model.M{
2✔
91
                        "terms": model.M{
2✔
92
                                model.FieldNameID: searchParams.DeviceIDs,
2✔
93
                        },
2✔
94
                })
2✔
95
        }
2✔
96

97
        esRes, err := app.store.Search(ctx, query)
23✔
98
        if err != nil {
25✔
99
                return nil, 0, err
2✔
100
        }
2✔
101

102
        res, total, err := app.storeToInventoryDevs(ctx, searchParams.TenantID, esRes)
21✔
103
        if err != nil {
23✔
104
                return nil, 0, err
2✔
105
        }
2✔
106

107
        return res, total, err
19✔
108
}
109

110
func (app *app) mapSearchParams(ctx context.Context, searchParams *model.SearchParams) error {
25✔
111
        if len(searchParams.Filters) > 0 {
44✔
112
                attributes := make(inventory.DeviceAttributes, 0, len(searchParams.Attributes))
19✔
113
                for i := 0; i < len(searchParams.Filters); i++ {
38✔
114
                        attributes = append(attributes, inventory.DeviceAttribute{
19✔
115
                                Name:        searchParams.Filters[i].Attribute,
19✔
116
                                Scope:       searchParams.Filters[i].Scope,
19✔
117
                                Value:       searchParams.Filters[i].Value,
19✔
118
                                Description: &searchParams.Filters[i].Type,
19✔
119
                        })
19✔
120
                }
19✔
121
                attributes, err := app.mapper.MapInventoryAttributes(ctx, searchParams.TenantID,
19✔
122
                        attributes, false)
19✔
123
                if err != nil {
19✔
NEW
124
                        return err
×
NEW
125
                }
×
126
                searchParams.Filters = make([]model.FilterPredicate, 0, len(searchParams.Filters))
19✔
127
                for _, attribute := range attributes {
37✔
128
                        searchParams.Filters = append(searchParams.Filters, model.FilterPredicate{
18✔
129
                                Attribute: attribute.Name,
18✔
130
                                Scope:     attribute.Scope,
18✔
131
                                Value:     attribute.Value,
18✔
132
                                Type:      *attribute.Description,
18✔
133
                        })
18✔
134
                }
18✔
135
        }
136
        if len(searchParams.Attributes) > 0 {
25✔
NEW
137
                attributes := make(inventory.DeviceAttributes, 0, len(searchParams.Attributes))
×
NEW
138
                for i := 0; i < len(searchParams.Attributes); i++ {
×
NEW
139
                        attributes = append(attributes, inventory.DeviceAttribute{
×
NEW
140
                                Name:  searchParams.Attributes[i].Attribute,
×
NEW
141
                                Scope: searchParams.Attributes[i].Scope,
×
NEW
142
                        })
×
NEW
143
                }
×
NEW
144
                attributes, err := app.mapper.MapInventoryAttributes(ctx, searchParams.TenantID,
×
NEW
145
                        attributes, false)
×
NEW
146
                if err != nil {
×
NEW
147
                        return err
×
NEW
148
                }
×
NEW
149
                searchParams.Attributes = make([]model.SelectAttribute, 0, len(searchParams.Attributes))
×
NEW
150
                for _, attribute := range attributes {
×
NEW
151
                        searchParams.Attributes = append(searchParams.Attributes, model.SelectAttribute{
×
NEW
152
                                Attribute: attribute.Name,
×
NEW
153
                                Scope:     attribute.Scope,
×
NEW
154
                        })
×
NEW
155
                }
×
156
        }
157
        if len(searchParams.Sort) > 0 {
36✔
158
                attributes := make(inventory.DeviceAttributes, 0, len(searchParams.Sort))
11✔
159
                for i := 0; i < len(searchParams.Sort); i++ {
22✔
160
                        attributes = append(attributes, inventory.DeviceAttribute{
11✔
161
                                Name:        searchParams.Sort[i].Attribute,
11✔
162
                                Scope:       searchParams.Sort[i].Scope,
11✔
163
                                Description: &searchParams.Sort[i].Order,
11✔
164
                        })
11✔
165
                }
11✔
166
                attributes, err := app.mapper.MapInventoryAttributes(ctx, searchParams.TenantID,
11✔
167
                        attributes, false)
11✔
168
                if err != nil {
11✔
NEW
169
                        return err
×
NEW
170
                }
×
171
                searchParams.Sort = make([]model.SortCriteria, 0, len(searchParams.Attributes))
11✔
172
                for _, attribute := range attributes {
22✔
173
                        searchParams.Sort = append(searchParams.Sort, model.SortCriteria{
11✔
174
                                Attribute: attribute.Name,
11✔
175
                                Scope:     attribute.Scope,
11✔
176
                                Order:     *attribute.Description,
11✔
177
                        })
11✔
178
                }
11✔
179
        }
180

181
        return nil
25✔
182
}
183

184
// storeToInventoryDevs translates ES results directly to iventory devices
185
func (a *app) storeToInventoryDevs(
186
        ctx context.Context, tenantID string, storeRes map[string]interface{},
187
) ([]inventory.Device, int, error) {
21✔
188
        devs := []inventory.Device{}
21✔
189

21✔
190
        hitsM, ok := storeRes["hits"].(map[string]interface{})
21✔
191
        if !ok {
21✔
192
                return nil, 0, errors.New("can't process store hits map")
×
193
        }
×
194

195
        hitsTotalM, ok := hitsM["total"].(map[string]interface{})
21✔
196
        if !ok {
21✔
197
                return nil, 0, errors.New("can't process total hits struct")
×
198
        }
×
199

200
        total, ok := hitsTotalM["value"].(float64)
21✔
201
        if !ok {
23✔
202
                return nil, 0, errors.New("can't process total hits value")
2✔
203
        }
2✔
204

205
        hitsS, ok := hitsM["hits"].([]interface{})
19✔
206
        if !ok {
19✔
207
                return nil, 0, errors.New("can't process store hits slice")
×
208
        }
×
209

210
        for _, v := range hitsS {
48✔
211
                res, err := a.storeToInventoryDev(ctx, tenantID, v)
29✔
212
                if err != nil {
29✔
213
                        return nil, 0, err
×
214
                }
×
215

216
                devs = append(devs, *res)
29✔
217
        }
218

219
        return devs, int(total), nil
19✔
220
}
221

222
func (a *app) storeToInventoryDev(ctx context.Context, tenantID string,
223
        storeRes interface{}) (*inventory.Device, error) {
29✔
224
        resM, ok := storeRes.(map[string]interface{})
29✔
225
        if !ok {
29✔
226
                return nil, errors.New("can't process individual hit")
×
227
        }
×
228

229
        // if query has a 'fields' clause, use 'fields' instead of '_source'
230
        sourceM, ok := resM["_source"].(map[string]interface{})
29✔
231
        if !ok {
29✔
232
                sourceM, ok = resM["fields"].(map[string]interface{})
×
233
                if !ok {
×
234
                        return nil, errors.New("can't process hit's '_source' nor 'fields'")
×
235
                }
×
236
        }
237

238
        // if query has a 'fields' clause, all results will be arrays incl. device id, so extract it
239
        id, ok := sourceM["id"].(string)
29✔
240
        if !ok {
29✔
241
                idarr, ok := sourceM["id"].([]interface{})
×
242
                if !ok {
×
243
                        return nil, errors.New(
×
244
                                "can't parse device id as neither single value nor array",
×
245
                        )
×
246
                }
×
247

248
                id, ok = idarr[0].(string)
×
249
                if !ok {
×
250
                        return nil, errors.New(
×
251
                                "can't parse device id as neither single value nor array",
×
252
                        )
×
253
                }
×
254
        }
255

256
        ret := &inventory.Device{
29✔
257
                ID: inventory.DeviceID(id),
29✔
258
        }
29✔
259

29✔
260
        attrs := []inventory.DeviceAttribute{}
29✔
261

29✔
262
        for k, v := range sourceM {
164✔
263
                s, n, err := model.MaybeParseAttr(k)
135✔
264
                if err != nil {
135✔
265
                        return nil, err
×
266
                }
×
267

268
                if vArray, ok := v.([]interface{}); ok && len(vArray) == 1 {
210✔
269
                        v = vArray[0]
75✔
270
                }
75✔
271

272
                if n != "" {
212✔
273
                        a := inventory.DeviceAttribute{
77✔
274
                                Name:  model.Redot(n),
77✔
275
                                Scope: s,
77✔
276
                                Value: v,
77✔
277
                        }
77✔
278

77✔
279
                        if a.Scope == model.ScopeSystem &&
77✔
280
                                a.Name == model.AttrNameUpdatedAt {
77✔
281
                                ret.UpdatedTs = parseTime(v)
×
282
                        } else if a.Scope == model.ScopeSystem &&
77✔
283
                                a.Name == model.AttrNameCreatedAt {
77✔
284
                                ret.CreatedTs = parseTime(v)
×
285
                        }
×
286

287
                        attrs = append(attrs, a)
77✔
288
                }
289
        }
290

291
        attributes, err := a.mapper.ReverseInventoryAttributes(ctx, tenantID, attrs)
29✔
292
        if err != nil {
29✔
NEW
293
                return nil, err
×
NEW
294
        }
×
295
        ret.Attributes = attributes
29✔
296

29✔
297
        return ret, nil
29✔
298
}
299

300
func parseTime(v interface{}) time.Time {
×
301
        val, _ := v.(string)
×
302
        if t, err := time.Parse(time.RFC3339, val); err == nil {
×
303
                return t
×
304
        }
×
305
        return time.Time{}
×
306
}
307

308
func (app *app) GetSearchableInvAttrs(
309
        ctx context.Context,
310
        tid string,
311
) ([]model.FilterAttribute, error) {
×
312
        l := log.FromContext(ctx)
×
313

×
314
        index, err := app.store.GetDevicesIndexMapping(ctx, tid)
×
315
        if err != nil {
×
316
                return nil, err
×
317
        }
×
318

319
        // inventory attributes are under 'mappings.properties'
320
        mappings, ok := index["mappings"]
×
321
        if !ok {
×
322
                return nil, errors.New("can't parse index mappings")
×
323
        }
×
324

325
        mappingsM, ok := mappings.(map[string]interface{})
×
326
        if !ok {
×
327
                return nil, errors.New("can't parse index mappings")
×
328
        }
×
329

330
        props, ok := mappingsM["properties"]
×
331
        if !ok {
×
332
                return nil, errors.New("can't parse index properties")
×
333
        }
×
334

335
        propsM, ok := props.(map[string]interface{})
×
336
        if !ok {
×
337
                return nil, errors.New("can't parse index properties")
×
338
        }
×
339

340
        ret := []model.FilterAttribute{}
×
341

×
342
        for k := range propsM {
×
343
                s, n, err := model.MaybeParseAttr(k)
×
344

×
345
                if err != nil {
×
346
                        return nil, err
×
347
                }
×
348

349
                if n != "" {
×
350
                        ret = append(ret, model.FilterAttribute{Name: n, Scope: s, Count: 1})
×
351
                }
×
352
        }
353

354
        sort.Slice(ret, func(i, j int) bool {
×
355
                if ret[j].Scope > ret[i].Scope {
×
356
                        return true
×
357
                }
×
358

359
                if ret[j].Scope < ret[i].Scope {
×
360
                        return false
×
361
                }
×
362

363
                return ret[j].Name > ret[i].Name
×
364
        })
365

366
        l.Debugf("parsed searchable attributes %v\n", ret)
×
367

×
368
        return ret, nil
×
369
}
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