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

mendersoftware / inventory / 1112831686

19 Dec 2023 01:57PM UTC coverage: 91.759% (+0.2%) from 91.521%
1112831686

Pull #435

gitlab-ci

alfrunes
fix: Bound the aggregation to get filter attributes

Changelog: Title
Ticket: MEN-6917
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #435: fix: Bound the aggregation to get filter attributes

26 of 28 new or added lines in 1 file covered. (92.86%)

53 existing lines in 1 file now uncovered.

2661 of 2900 relevant lines covered (91.76%)

119.29 hits per line

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

90.28
/store/mongo/datastore_mongo.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 mongo
16

17
import (
18
        "context"
19
        "crypto/tls"
20
        "fmt"
21
        "io"
22
        "strings"
23
        "sync"
24
        "time"
25

26
        "go.mongodb.org/mongo-driver/bson"
27
        "go.mongodb.org/mongo-driver/bson/primitive"
28
        "go.mongodb.org/mongo-driver/mongo"
29
        mopts "go.mongodb.org/mongo-driver/mongo/options"
30

31
        "github.com/google/uuid"
32
        "github.com/pkg/errors"
33

34
        "github.com/mendersoftware/go-lib-micro/log"
35
        mstore "github.com/mendersoftware/go-lib-micro/store"
36

37
        "github.com/mendersoftware/inventory/model"
38
        "github.com/mendersoftware/inventory/store"
39
        "github.com/mendersoftware/inventory/utils"
40
)
41

42
const (
43
        DbVersion = "1.1.0"
44

45
        DbName        = "inventory"
46
        DbDevicesColl = "devices"
47

48
        DbDevId              = "_id"
49
        DbDevAttributes      = "attributes"
50
        DbDevGroup           = "group"
51
        DbDevRevision        = "revision"
52
        DbDevUpdatedTs       = "updated_ts"
53
        DbDevAttributesText  = "text"
54
        DbDevAttributesTs    = "timestamp"
55
        DbDevAttributesDesc  = "description"
56
        DbDevAttributesValue = "value"
57
        DbDevAttributesScope = "scope"
58
        DbDevAttributesName  = "name"
59
        DbDevAttributesGroup = DbDevAttributes + "." +
60
                model.AttrScopeSystem + "-" + model.AttrNameGroup
61
        DbDevAttributesGroupValue = DbDevAttributesGroup + "." +
62
                DbDevAttributesValue
63

64
        DbScopeInventory = "inventory"
65

66
        FiltersAttributesLimit = 500
67

68
        attrIdentityStatus = "identity-status"
69
)
70

71
var (
72
        //with offcial mongodb supported driver we keep client
73
        clientGlobal *mongo.Client
74

75
        // once ensures client is created only once
76
        once sync.Once
77

78
        ErrNotFound = errors.New("mongo: no documents in result")
79
)
80

81
type DataStoreMongoConfig struct {
82
        // connection string
83
        ConnectionString string
84

85
        // SSL support
86
        SSL           bool
87
        SSLSkipVerify bool
88

89
        // Overwrites credentials provided in connection string if provided
90
        Username string
91
        Password string
92
}
93

94
type DataStoreMongo struct {
95
        client      *mongo.Client
96
        automigrate bool
97
}
98

99
func NewDataStoreMongoWithSession(client *mongo.Client) store.DataStore {
195✔
100
        return &DataStoreMongo{client: client}
195✔
101
}
195✔
102

103
// config.ConnectionString must contain a valid
104
func NewDataStoreMongo(config DataStoreMongoConfig) (store.DataStore, error) {
5✔
105
        //init master session
5✔
106
        var err error
5✔
107
        once.Do(func() {
9✔
108
                if !strings.Contains(config.ConnectionString, "://") {
8✔
109
                        config.ConnectionString = "mongodb://" + config.ConnectionString
4✔
110
                }
4✔
111
                clientOptions := mopts.Client().ApplyURI(config.ConnectionString)
4✔
112

4✔
113
                if config.Username != "" {
4✔
UNCOV
114
                        clientOptions.SetAuth(mopts.Credential{
×
115
                                Username: config.Username,
×
116
                                Password: config.Password,
×
117
                        })
×
118
                }
×
119

120
                if config.SSL {
4✔
UNCOV
121
                        tlsConfig := &tls.Config{}
×
122
                        tlsConfig.InsecureSkipVerify = config.SSLSkipVerify
×
123
                        clientOptions.SetTLSConfig(tlsConfig)
×
124
                }
×
125

126
                ctx := context.Background()
4✔
127
                l := log.FromContext(ctx)
4✔
128
                clientGlobal, err = mongo.Connect(ctx, clientOptions)
4✔
129
                if err != nil {
5✔
130
                        l.Errorf("mongo: error connecting to mongo '%s'", err.Error())
1✔
131
                        return
1✔
132
                }
1✔
133
                if clientGlobal == nil {
3✔
UNCOV
134
                        l.Errorf("mongo: client is nil. wow.")
×
135
                        return
×
136
                }
×
137
                // from: https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial
138
                /*
139
                        It is best practice to keep a client that is connected to MongoDB around so that the
140
                        application can make use of connection pooling - you don't want to open and close a
141
                        connection for each query. However, if your application no longer requires a connection,
142
                        the connection can be closed with client.Disconnect() like so:
143
                */
144
                err = clientGlobal.Ping(ctx, nil)
3✔
145
                if err != nil {
3✔
UNCOV
146
                        clientGlobal = nil
×
147
                        l.Errorf("mongo: error pinging mongo '%s'", err.Error())
×
148
                        return
×
149
                }
×
150
                if clientGlobal == nil {
3✔
UNCOV
151
                        l.Errorf("mongo: global instance of client is nil.")
×
152
                        return
×
153
                }
×
154
        })
155

156
        if clientGlobal == nil {
6✔
157
                return nil, errors.New("failed to open mongo-driver session")
1✔
158
        }
1✔
159
        db := &DataStoreMongo{client: clientGlobal}
4✔
160

4✔
161
        return db, nil
4✔
162
}
163

164
func (db *DataStoreMongo) Ping(ctx context.Context) error {
1✔
165
        res := db.client.Database(DbName).RunCommand(ctx, bson.M{"ping": 1})
1✔
166
        return res.Err()
1✔
167
}
1✔
168

169
func (db *DataStoreMongo) GetDevices(
170
        ctx context.Context,
171
        q store.ListQuery,
172
) ([]model.Device, int, error) {
53✔
173
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
53✔
174

53✔
175
        queryFilters := make([]bson.M, 0)
53✔
176
        for _, filter := range q.Filters {
60✔
177
                op := mongoOperator(filter.Operator)
7✔
178
                name := fmt.Sprintf(
7✔
179
                        "%s-%s",
7✔
180
                        filter.AttrScope,
7✔
181
                        model.GetDeviceAttributeNameReplacer().Replace(filter.AttrName),
7✔
182
                )
7✔
183
                field := fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
7✔
184
                switch filter.Operator {
7✔
185
                default:
7✔
186
                        if filter.ValueFloat != nil {
11✔
187
                                queryFilters = append(queryFilters, bson.M{"$or": []bson.M{
4✔
188
                                        {field: bson.M{op: filter.Value}},
4✔
189
                                        {field: bson.M{op: filter.ValueFloat}},
4✔
190
                                }})
4✔
191
                        } else if filter.ValueTime != nil {
8✔
192
                                queryFilters = append(queryFilters, bson.M{"$or": []bson.M{
1✔
193
                                        {field: bson.M{op: filter.Value}},
1✔
194
                                        {field: bson.M{op: filter.ValueTime}},
1✔
195
                                }})
1✔
196
                        } else {
3✔
197
                                queryFilters = append(queryFilters, bson.M{field: bson.M{op: filter.Value}})
2✔
198
                        }
2✔
199
                }
200
        }
201
        if q.GroupName != "" {
85✔
202
                groupFilter := bson.M{DbDevAttributesGroupValue: q.GroupName}
32✔
203
                queryFilters = append(queryFilters, groupFilter)
32✔
204
        }
32✔
205
        if q.GroupName != "" {
85✔
206
                groupFilter := bson.M{DbDevAttributesGroupValue: q.GroupName}
32✔
207
                queryFilters = append(queryFilters, groupFilter)
32✔
208
        }
32✔
209
        if q.HasGroup != nil {
89✔
210
                groupExistenceFilter := bson.M{
36✔
211
                        DbDevAttributesGroup: bson.M{
36✔
212
                                "$exists": *q.HasGroup,
36✔
213
                        },
36✔
214
                }
36✔
215
                queryFilters = append(queryFilters, groupExistenceFilter)
36✔
216
        }
36✔
217

218
        findQuery := bson.M{}
53✔
219
        if len(queryFilters) > 0 {
96✔
220
                findQuery["$and"] = queryFilters
43✔
221
        }
43✔
222

223
        findOptions := mopts.Find()
53✔
224
        if q.Skip > 0 {
63✔
225
                findOptions.SetSkip(int64(q.Skip))
10✔
226
        }
10✔
227
        if q.Limit > 0 {
101✔
228
                findOptions.SetLimit(int64(q.Limit))
48✔
229
        }
48✔
230
        if q.Sort != nil {
58✔
231
                name := fmt.Sprintf(
5✔
232
                        "%s-%s",
5✔
233
                        q.Sort.AttrScope,
5✔
234
                        model.GetDeviceAttributeNameReplacer().Replace(q.Sort.AttrName),
5✔
235
                )
5✔
236
                sortField := fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
5✔
237
                sortFieldQuery := bson.D{{Key: sortField, Value: 1}}
5✔
238
                if !q.Sort.Ascending {
8✔
239
                        sortFieldQuery[0].Value = -1
3✔
240
                }
3✔
241
                findOptions.SetSort(sortFieldQuery)
5✔
242
        }
243

244
        cursor, err := c.Find(ctx, findQuery, findOptions)
53✔
245
        if err != nil {
53✔
UNCOV
246
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
247
        }
×
248
        defer cursor.Close(ctx)
53✔
249

53✔
250
        devices := []model.Device{}
53✔
251
        if err = cursor.All(ctx, &devices); err != nil {
53✔
UNCOV
252
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
253
        }
×
254

255
        count, err := c.CountDocuments(ctx, findQuery)
53✔
256
        if err != nil {
53✔
UNCOV
257
                return nil, -1, errors.Wrap(err, "failed to count devices")
×
258
        }
×
259

260
        return devices, int(count), nil
53✔
261
}
262

263
func (db *DataStoreMongo) GetDevice(
264
        ctx context.Context,
265
        id model.DeviceID,
266
) (*model.Device, error) {
41✔
267
        var res model.Device
41✔
268
        c := db.client.
41✔
269
                Database(mstore.DbFromContext(ctx, DbName)).
41✔
270
                Collection(DbDevicesColl)
41✔
271
        l := log.FromContext(ctx)
41✔
272

41✔
273
        if id == model.NilDeviceID {
43✔
274
                return nil, nil
2✔
275
        }
2✔
276
        if err := c.FindOne(ctx, bson.M{DbDevId: id}).Decode(&res); err != nil {
43✔
277
                switch err {
4✔
278
                case mongo.ErrNoDocuments:
4✔
279
                        return nil, nil
4✔
UNCOV
280
                default:
×
281
                        l.Errorf("GetDevice: %v", err)
×
282
                        return nil, errors.Wrap(err, "failed to fetch device")
×
283
                }
284
        }
285
        return &res, nil
35✔
286
}
287

288
// AddDevice inserts a new device, initializing the inventory data.
289
func (db *DataStoreMongo) AddDevice(ctx context.Context, dev *model.Device) error {
338✔
290
        if dev.Group != "" {
458✔
291
                dev.Attributes = append(dev.Attributes, model.DeviceAttribute{
120✔
292
                        Scope: model.AttrScopeSystem,
120✔
293
                        Name:  model.AttrNameGroup,
120✔
294
                        Value: dev.Group,
120✔
295
                })
120✔
296
        }
120✔
297
        _, err := db.UpsertDevicesAttributesWithUpdated(
338✔
298
                ctx, []model.DeviceID{dev.ID}, dev.Attributes, "", "",
338✔
299
        )
338✔
300
        if err != nil {
338✔
UNCOV
301
                return errors.Wrap(err, "failed to store device")
×
302
        }
×
303
        return nil
338✔
304
}
305

306
func (db *DataStoreMongo) UpsertDevicesAttributesWithRevision(
307
        ctx context.Context,
308
        devices []model.DeviceUpdate,
309
        attrs model.DeviceAttributes,
310
) (*model.UpdateResult, error) {
3✔
311
        return db.upsertAttributes(ctx, devices, attrs, false, true, "", "")
3✔
312
}
3✔
313

314
func (db *DataStoreMongo) UpsertDevicesAttributesWithUpdated(
315
        ctx context.Context,
316
        ids []model.DeviceID,
317
        attrs model.DeviceAttributes,
318
        scope string,
319
        etag string,
320
) (*model.UpdateResult, error) {
360✔
321
        withUpdated := scope == model.AttrScopeInventory
360✔
322
        return db.upsertAttributes(ctx, makeDevsWithIds(ids), attrs, withUpdated, false, scope, etag)
360✔
323
}
360✔
324

325
func (db *DataStoreMongo) UpsertDevicesAttributes(
326
        ctx context.Context,
327
        ids []model.DeviceID,
328
        attrs model.DeviceAttributes,
329
) (*model.UpdateResult, error) {
17✔
330
        return db.upsertAttributes(ctx, makeDevsWithIds(ids), attrs, false, false, "", "")
17✔
331
}
17✔
332

333
func makeDevsWithIds(ids []model.DeviceID) []model.DeviceUpdate {
377✔
334
        devices := make([]model.DeviceUpdate, len(ids))
377✔
335
        for i, id := range ids {
754✔
336
                devices[i].Id = id
377✔
337
        }
377✔
338
        return devices
377✔
339
}
340

341
func (db *DataStoreMongo) upsertAttributes(
342
        ctx context.Context,
343
        devices []model.DeviceUpdate,
344
        attrs model.DeviceAttributes,
345
        withUpdated bool,
346
        withRevision bool,
347
        scope string,
348
        etag string,
349
) (*model.UpdateResult, error) {
380✔
350
        const systemScope = DbDevAttributes + "." + model.AttrScopeSystem
380✔
351
        const createdField = systemScope + "-" + model.AttrNameCreated
380✔
352
        const etagField = model.AttrNameTagsEtag
380✔
353
        var (
380✔
354
                result *model.UpdateResult
380✔
355
                filter interface{}
380✔
356
                err    error
380✔
357
        )
380✔
358

380✔
359
        c := db.client.
380✔
360
                Database(mstore.DbFromContext(ctx, DbName)).
380✔
361
                Collection(DbDevicesColl)
380✔
362

380✔
363
        update, err := makeAttrUpsert(attrs)
380✔
364
        if err != nil {
382✔
365
                return nil, err
2✔
366
        }
2✔
367

368
        now := time.Now()
378✔
369
        oninsert := bson.M{
378✔
370
                createdField: model.DeviceAttribute{
378✔
371
                        Scope: model.AttrScopeSystem,
378✔
372
                        Name:  model.AttrNameCreated,
378✔
373
                        Value: now,
378✔
374
                },
378✔
375
        }
378✔
376
        if !withRevision {
753✔
377
                oninsert["revision"] = 0
375✔
378
        }
375✔
379

380
        const updatedField = systemScope + "-" + model.AttrNameUpdated
378✔
381
        if withUpdated {
391✔
382
                update[updatedField] = model.DeviceAttribute{
13✔
383
                        Scope: model.AttrScopeSystem,
13✔
384
                        Name:  model.AttrNameUpdated,
13✔
385
                        Value: now,
13✔
386
                }
13✔
387
        } else {
378✔
388
                oninsert[updatedField] = model.DeviceAttribute{
365✔
389
                        Scope: model.AttrScopeSystem,
365✔
390
                        Name:  model.AttrNameUpdated,
365✔
391
                        Value: now,
365✔
392
                }
365✔
393
        }
365✔
394

395
        switch len(devices) {
378✔
396
        case 0:
2✔
397
                return &model.UpdateResult{}, nil
2✔
398
        case 1:
373✔
399
                filter := bson.M{
373✔
400
                        "_id": devices[0].Id,
373✔
401
                }
373✔
402
                updateOpts := mopts.FindOneAndUpdate().
373✔
403
                        SetUpsert(true).
373✔
404
                        SetReturnDocument(mopts.After)
373✔
405

373✔
406
                if withRevision {
375✔
407
                        filter[DbDevRevision] = bson.M{"$lt": devices[0].Revision}
2✔
408
                        update[DbDevRevision] = devices[0].Revision
2✔
409
                }
2✔
410
                if scope == model.AttrScopeTags {
379✔
411
                        update[etagField] = uuid.New().String()
6✔
412
                        updateOpts = mopts.FindOneAndUpdate().
6✔
413
                                SetUpsert(false).
6✔
414
                                SetReturnDocument(mopts.After)
6✔
415
                }
6✔
416
                if etag != "" {
374✔
417
                        filter[etagField] = bson.M{"$eq": etag}
1✔
418
                }
1✔
419

420
                update = bson.M{
373✔
421
                        "$set":         update,
373✔
422
                        "$setOnInsert": oninsert,
373✔
423
                }
373✔
424

373✔
425
                device := &model.Device{}
373✔
426
                res := c.FindOneAndUpdate(ctx, filter, update, updateOpts)
373✔
427
                err = res.Decode(device)
373✔
428
                if err != nil {
375✔
429
                        if mongo.IsDuplicateKeyError(err) {
3✔
430
                                return nil, store.ErrWriteConflict
1✔
431
                        } else if err == mongo.ErrNoDocuments {
3✔
432
                                return &model.UpdateResult{}, nil
1✔
433
                        } else {
1✔
UNCOV
434
                                return nil, err
×
435
                        }
×
436
                }
437
                result = &model.UpdateResult{
371✔
438
                        MatchedCount: 1,
371✔
439
                        CreatedCount: 0,
371✔
440
                        Devices:      []*model.Device{device},
371✔
441
                }
371✔
442
        default:
3✔
443
                var bres *mongo.BulkWriteResult
3✔
444
                // Perform single bulk-write operation
3✔
445
                // NOTE: Can't use UpdateMany as $in query operator does not
3✔
446
                //       upsert missing devices.
3✔
447

3✔
448
                models := make([]mongo.WriteModel, len(devices))
3✔
449
                for i, dev := range devices {
12✔
450
                        umod := mongo.NewUpdateOneModel()
9✔
451
                        if withRevision {
12✔
452
                                filter = bson.M{
3✔
453
                                        "_id":         dev.Id,
3✔
454
                                        DbDevRevision: bson.M{"$lt": dev.Revision},
3✔
455
                                }
3✔
456
                                update[DbDevRevision] = dev.Revision
3✔
457
                                umod.Update = bson.M{
3✔
458
                                        "$set":         update,
3✔
459
                                        "$setOnInsert": oninsert,
3✔
460
                                }
3✔
461
                        } else {
9✔
462
                                filter = map[string]interface{}{"_id": dev.Id}
6✔
463
                                umod.Update = bson.M{
6✔
464
                                        "$set":         update,
6✔
465
                                        "$setOnInsert": oninsert,
6✔
466
                                }
6✔
467
                        }
6✔
468
                        umod.Filter = filter
9✔
469
                        umod.SetUpsert(true)
9✔
470
                        models[i] = umod
9✔
471
                }
472
                bres, err = c.BulkWrite(
3✔
473
                        ctx, models, mopts.BulkWrite().SetOrdered(false),
3✔
474
                )
3✔
475
                if err != nil {
4✔
476
                        if mongo.IsDuplicateKeyError(err) {
2✔
477
                                // bulk mode, swallow the error as we already updated the other devices
1✔
478
                                // and the Matchedcount and CreatedCount values will tell the caller if
1✔
479
                                // all the operations succeeded or not
1✔
480
                                err = nil
1✔
481
                        } else {
1✔
UNCOV
482
                                return nil, err
×
483
                        }
×
484
                }
485
                result = &model.UpdateResult{
3✔
486
                        MatchedCount: bres.MatchedCount,
3✔
487
                        CreatedCount: bres.UpsertedCount,
3✔
488
                }
3✔
489
        }
490
        return result, err
374✔
491
}
492

493
// makeAttrField is a convenience function for composing attribute field names.
494
func makeAttrField(attrName, attrScope string, subFields ...string) string {
6,388✔
495
        field := fmt.Sprintf(
6,388✔
496
                "%s.%s-%s",
6,388✔
497
                DbDevAttributes,
6,388✔
498
                attrScope,
6,388✔
499
                model.GetDeviceAttributeNameReplacer().Replace(attrName),
6,388✔
500
        )
6,388✔
501
        if len(subFields) > 0 {
12,775✔
502
                field = strings.Join(
6,387✔
503
                        append([]string{field}, subFields...), ".",
6,387✔
504
                )
6,387✔
505
        }
6,387✔
506
        return field
6,388✔
507
}
508

509
// makeAttrUpsert creates a new upsert document for the given attributes.
510
func makeAttrUpsert(attrs model.DeviceAttributes) (bson.M, error) {
407✔
511
        var fieldName string
407✔
512
        upsert := make(bson.M)
407✔
513

407✔
514
        for i := range attrs {
2,276✔
515
                if attrs[i].Name == "" {
1,872✔
516
                        return nil, store.ErrNoAttrName
3✔
517
                }
3✔
518
                if attrs[i].Scope == "" {
1,872✔
519
                        // Default to inventory scope
6✔
520
                        attrs[i].Scope = model.AttrScopeInventory
6✔
521
                }
6✔
522

523
                fieldName = makeAttrField(
1,866✔
524
                        attrs[i].Name,
1,866✔
525
                        attrs[i].Scope,
1,866✔
526
                        DbDevAttributesScope,
1,866✔
527
                )
1,866✔
528
                upsert[fieldName] = attrs[i].Scope
1,866✔
529

1,866✔
530
                fieldName = makeAttrField(
1,866✔
531
                        attrs[i].Name,
1,866✔
532
                        attrs[i].Scope,
1,866✔
533
                        DbDevAttributesName,
1,866✔
534
                )
1,866✔
535
                upsert[fieldName] = attrs[i].Name
1,866✔
536

1,866✔
537
                if attrs[i].Value != nil {
3,727✔
538
                        fieldName = makeAttrField(
1,861✔
539
                                attrs[i].Name,
1,861✔
540
                                attrs[i].Scope,
1,861✔
541
                                DbDevAttributesValue,
1,861✔
542
                        )
1,861✔
543
                        upsert[fieldName] = attrs[i].Value
1,861✔
544
                }
1,861✔
545

546
                if attrs[i].Description != nil {
2,568✔
547
                        fieldName = makeAttrField(
702✔
548
                                attrs[i].Name,
702✔
549
                                attrs[i].Scope,
702✔
550
                                DbDevAttributesDesc,
702✔
551
                        )
702✔
552
                        upsert[fieldName] = attrs[i].Description
702✔
553
                }
702✔
554

555
                if attrs[i].Timestamp != nil {
1,958✔
556
                        fieldName = makeAttrField(
92✔
557
                                attrs[i].Name,
92✔
558
                                attrs[i].Scope,
92✔
559
                                DbDevAttributesTs,
92✔
560
                        )
92✔
561
                        upsert[fieldName] = attrs[i].Timestamp
92✔
562
                }
92✔
563
        }
564
        return upsert, nil
404✔
565
}
566

567
// makeAttrRemove creates a new unset document to remove attributes
568
func makeAttrRemove(attrs model.DeviceAttributes) (bson.M, error) {
26✔
569
        var fieldName string
26✔
570
        remove := make(bson.M)
26✔
571

26✔
572
        for i := range attrs {
27✔
573
                if attrs[i].Name == "" {
1✔
UNCOV
574
                        return nil, store.ErrNoAttrName
×
575
                }
×
576
                if attrs[i].Scope == "" {
1✔
UNCOV
577
                        // Default to inventory scope
×
578
                        attrs[i].Scope = model.AttrScopeInventory
×
579
                }
×
580
                fieldName = makeAttrField(
1✔
581
                        attrs[i].Name,
1✔
582
                        attrs[i].Scope,
1✔
583
                )
1✔
584
                remove[fieldName] = true
1✔
585
        }
586
        return remove, nil
26✔
587
}
588

589
func mongoOperator(co store.ComparisonOperator) string {
7✔
590
        switch co {
7✔
591
        case store.Eq:
7✔
592
                return "$eq"
7✔
593
        }
UNCOV
594
        return ""
×
595
}
596

597
func (db *DataStoreMongo) UpsertRemoveDeviceAttributes(
598
        ctx context.Context,
599
        id model.DeviceID,
600
        updateAttrs model.DeviceAttributes,
601
        removeAttrs model.DeviceAttributes,
602
        scope string,
603
        etag string,
604
) (*model.UpdateResult, error) {
27✔
605
        const systemScope = DbDevAttributes + "." + model.AttrScopeSystem
27✔
606
        const updatedField = systemScope + "-" + model.AttrNameUpdated
27✔
607
        const createdField = systemScope + "-" + model.AttrNameCreated
27✔
608
        const etagField = model.AttrNameTagsEtag
27✔
609
        var (
27✔
610
                err error
27✔
611
        )
27✔
612

27✔
613
        c := db.client.
27✔
614
                Database(mstore.DbFromContext(ctx, DbName)).
27✔
615
                Collection(DbDevicesColl)
27✔
616

27✔
617
        update, err := makeAttrUpsert(updateAttrs)
27✔
618
        if err != nil {
28✔
619
                return nil, err
1✔
620
        }
1✔
621
        remove, err := makeAttrRemove(removeAttrs)
26✔
622
        if err != nil {
26✔
UNCOV
623
                return nil, err
×
624
        }
×
625
        filter := bson.M{"_id": id}
26✔
626
        if etag != "" {
31✔
627
                filter[etagField] = bson.M{"$eq": etag}
5✔
628
        }
5✔
629

630
        updateOpts := mopts.FindOneAndUpdate().
26✔
631
                SetUpsert(true).
26✔
632
                SetReturnDocument(mopts.After)
26✔
633
        if scope == model.AttrScopeTags {
38✔
634
                update[etagField] = uuid.New().String()
12✔
635
                updateOpts = updateOpts.SetUpsert(false)
12✔
636
        }
12✔
637
        now := time.Now()
26✔
638
        if scope != model.AttrScopeTags {
40✔
639
                update[updatedField] = model.DeviceAttribute{
14✔
640
                        Scope: model.AttrScopeSystem,
14✔
641
                        Name:  model.AttrNameUpdated,
14✔
642
                        Value: now,
14✔
643
                }
14✔
644
        }
14✔
645
        update = bson.M{
26✔
646
                "$set": update,
26✔
647
                "$setOnInsert": bson.M{
26✔
648
                        createdField: model.DeviceAttribute{
26✔
649
                                Scope: model.AttrScopeSystem,
26✔
650
                                Name:  model.AttrNameCreated,
26✔
651
                                Value: now,
26✔
652
                        },
26✔
653
                },
26✔
654
        }
26✔
655
        if len(remove) > 0 {
27✔
656
                update["$unset"] = remove
1✔
657
        }
1✔
658

659
        device := &model.Device{}
26✔
660
        res := c.FindOneAndUpdate(ctx, filter, update, updateOpts)
26✔
661
        err = res.Decode(device)
26✔
662
        if err == mongo.ErrNoDocuments {
28✔
663
                return &model.UpdateResult{
2✔
664
                        MatchedCount: 0,
2✔
665
                        CreatedCount: 0,
2✔
666
                        Devices:      []*model.Device{},
2✔
667
                }, nil
2✔
668
        } else if err == nil {
50✔
669
                return &model.UpdateResult{
24✔
670
                        MatchedCount: 1,
24✔
671
                        CreatedCount: 0,
24✔
672
                        Devices:      []*model.Device{device},
24✔
673
                }, nil
24✔
674
        }
24✔
UNCOV
675
        return nil, err
×
676
}
677

678
func (db *DataStoreMongo) UpdateDevicesGroup(
679
        ctx context.Context,
680
        devIDs []model.DeviceID,
681
        group model.GroupName,
682
) (*model.UpdateResult, error) {
61✔
683
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
61✔
684
        collDevs := database.Collection(DbDevicesColl)
61✔
685

61✔
686
        var filter = bson.M{}
61✔
687
        switch len(devIDs) {
61✔
688
        case 0:
3✔
689
                return &model.UpdateResult{}, nil
3✔
690
        case 1:
54✔
691
                filter[DbDevId] = devIDs[0]
54✔
692
        default:
4✔
693
                filter[DbDevId] = bson.M{"$in": devIDs}
4✔
694
        }
695
        update := bson.M{
58✔
696
                "$set": bson.M{
58✔
697
                        DbDevAttributesGroup: model.DeviceAttribute{
58✔
698
                                Scope: model.AttrScopeSystem,
58✔
699
                                Name:  DbDevGroup,
58✔
700
                                Value: group,
58✔
701
                        },
58✔
702
                },
58✔
703
        }
58✔
704
        res, err := collDevs.UpdateMany(ctx, filter, update)
58✔
705
        if err != nil {
58✔
UNCOV
706
                return nil, err
×
707
        }
×
708
        return &model.UpdateResult{
58✔
709
                MatchedCount: res.MatchedCount,
58✔
710
                UpdatedCount: res.ModifiedCount,
58✔
711
        }, nil
58✔
712
}
713

714
// UpdateDeviceText updates the device text field
715
func (db *DataStoreMongo) UpdateDeviceText(
716
        ctx context.Context,
717
        deviceID model.DeviceID,
718
        text string,
719
) error {
22✔
720
        filter := bson.M{
22✔
721
                DbDevId: deviceID.String(),
22✔
722
        }
22✔
723

22✔
724
        update := bson.M{
22✔
725
                "$set": bson.M{
22✔
726
                        DbDevAttributesText: text,
22✔
727
                },
22✔
728
        }
22✔
729

22✔
730
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
22✔
731
        collDevs := database.Collection(DbDevicesColl)
22✔
732

22✔
733
        _, err := collDevs.UpdateOne(ctx, filter, update)
22✔
734
        return err
22✔
735
}
22✔
736

737
func (db *DataStoreMongo) GetFiltersAttributes(
738
        ctx context.Context,
739
) ([]model.FilterAttribute, error) {
4✔
740
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
4✔
741
        collDevs := database.Collection(DbDevicesColl)
4✔
742

4✔
743
        const DbCount = "count"
4✔
744

4✔
745
        cur, err := collDevs.Aggregate(ctx, []bson.M{
4✔
746
                // Sample 500 devices to get a representative sample
4✔
747
                {
4✔
748
                        "$limit": 1000,
4✔
749
                },
4✔
750
                {
4✔
751
                        // Sort by _id to get a consistent random sample
4✔
752
                        "$sort": bson.M{
4✔
753
                                DbDevId: 1,
4✔
754
                        },
4✔
755
                },
4✔
756
                {
4✔
757
                        "$project": bson.M{
4✔
758
                                "attributes": bson.M{
4✔
759
                                        "$objectToArray": "$" + DbDevAttributes,
4✔
760
                                },
4✔
761
                        },
4✔
762
                },
4✔
763
                {
4✔
764
                        "$unwind": "$" + DbDevAttributes,
4✔
765
                },
4✔
766
                {
4✔
767
                        "$project": bson.M{
4✔
768
                                DbDevAttributesName:  "$" + DbDevAttributes + ".v." + DbDevAttributesName,
4✔
769
                                DbDevAttributesScope: "$" + DbDevAttributes + ".v." + DbDevAttributesScope,
4✔
770
                        },
4✔
771
                },
4✔
772
                {
4✔
773
                        "$group": bson.M{
4✔
774
                                DbDevId: bson.M{
4✔
775
                                        DbDevAttributesName:  "$" + DbDevAttributesName,
4✔
776
                                        DbDevAttributesScope: "$" + DbDevAttributesScope,
4✔
777
                                },
4✔
778
                                DbCount: bson.M{
4✔
779
                                        "$sum": 1,
4✔
780
                                },
4✔
781
                        },
4✔
782
                },
4✔
783
                {
4✔
784
                        "$project": bson.M{
4✔
785
                                DbDevId:              0,
4✔
786
                                DbDevAttributesName:  "$" + DbDevId + "." + DbDevAttributesName,
4✔
787
                                DbDevAttributesScope: "$" + DbDevId + "." + DbDevAttributesScope,
4✔
788
                                DbCount:              "$" + DbCount,
4✔
789
                        },
4✔
790
                },
4✔
791
                {
4✔
792
                        "$sort": bson.D{
4✔
793
                                {Key: DbCount, Value: -1},
4✔
794
                                {Key: DbDevAttributesScope, Value: 1},
4✔
795
                                {Key: DbDevAttributesName, Value: 1},
4✔
796
                        },
4✔
797
                },
4✔
798
                {
4✔
799
                        "$limit": FiltersAttributesLimit,
4✔
800
                },
4✔
801
        })
4✔
802
        if err != nil {
4✔
NEW
803
                return nil, err
×
NEW
804
        }
×
805
        defer cur.Close(ctx)
4✔
806

4✔
807
        var attributes []model.FilterAttribute
4✔
808
        err = cur.All(ctx, &attributes)
4✔
809
        if err != nil {
4✔
UNCOV
810
                return nil, err
×
UNCOV
811
        }
×
812

813
        return attributes, nil
4✔
814
}
815

816
func (db *DataStoreMongo) DeleteGroup(
817
        ctx context.Context,
818
        group model.GroupName,
819
) (chan model.DeviceID, error) {
1✔
820
        deviceIDs := make(chan model.DeviceID)
1✔
821

1✔
822
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
823
        collDevs := database.Collection(DbDevicesColl)
1✔
824

1✔
825
        filter := bson.M{DbDevAttributesGroupValue: group}
1✔
826

1✔
827
        const batchMaxSize = 100
1✔
828
        batchSize := int32(batchMaxSize)
1✔
829
        findOptions := &mopts.FindOptions{
1✔
830
                Projection: bson.M{DbDevId: 1},
1✔
831
                BatchSize:  &batchSize,
1✔
832
        }
1✔
833
        cursor, err := collDevs.Find(ctx, filter, findOptions)
1✔
834
        if err != nil {
1✔
UNCOV
835
                return nil, err
×
UNCOV
836
        }
×
837

838
        go func() {
2✔
839
                defer cursor.Close(ctx)
1✔
840
                batch := make([]model.DeviceID, batchMaxSize)
1✔
841
                batchSize := 0
1✔
842

1✔
843
                update := bson.M{"$unset": bson.M{DbDevAttributesGroup: 1}}
1✔
844
                device := &model.Device{}
1✔
845
                defer close(deviceIDs)
1✔
846

1✔
847
        next:
1✔
848
                for {
5✔
849
                        hasNext := cursor.Next(ctx)
4✔
850
                        if !hasNext {
6✔
851
                                if batchSize > 0 {
3✔
852
                                        break
1✔
853
                                }
854
                                return
1✔
855
                        }
856
                        if err = cursor.Decode(&device); err == nil {
4✔
857
                                batch[batchSize] = device.ID
2✔
858
                                batchSize++
2✔
859
                                if len(batch) == batchSize {
2✔
UNCOV
860
                                        break
×
861
                                }
862
                        }
863
                }
864

865
                _, _ = collDevs.UpdateMany(ctx, bson.M{DbDevId: bson.M{"$in": batch[:batchSize]}}, update)
1✔
866
                for _, item := range batch[:batchSize] {
3✔
867
                        deviceIDs <- item
2✔
868
                }
2✔
869
                batchSize = 0
1✔
870
                goto next
1✔
871
        }()
872

873
        return deviceIDs, nil
1✔
874
}
875

876
func (db *DataStoreMongo) UnsetDevicesGroup(
877
        ctx context.Context,
878
        deviceIDs []model.DeviceID,
879
        group model.GroupName,
880
) (*model.UpdateResult, error) {
14✔
881
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
14✔
882
        collDevs := database.Collection(DbDevicesColl)
14✔
883

14✔
884
        var filter bson.D
14✔
885
        // Add filter on device id (either $in or direct indexing)
14✔
886
        switch len(deviceIDs) {
14✔
887
        case 0:
1✔
888
                return &model.UpdateResult{}, nil
1✔
889
        case 1:
10✔
890
                filter = bson.D{{Key: DbDevId, Value: deviceIDs[0]}}
10✔
891
        default:
3✔
892
                filter = bson.D{{Key: DbDevId, Value: bson.M{"$in": deviceIDs}}}
3✔
893
        }
894
        // Append filter on group
895
        filter = append(
13✔
896
                filter,
13✔
897
                bson.E{Key: DbDevAttributesGroupValue, Value: group},
13✔
898
        )
13✔
899
        // Create unset operation on group attribute
13✔
900
        update := bson.M{
13✔
901
                "$unset": bson.M{
13✔
902
                        DbDevAttributesGroup: "",
13✔
903
                },
13✔
904
        }
13✔
905
        res, err := collDevs.UpdateMany(ctx, filter, update)
13✔
906
        if err != nil {
13✔
UNCOV
907
                return nil, err
×
UNCOV
908
        }
×
909
        return &model.UpdateResult{
13✔
910
                MatchedCount: res.MatchedCount,
13✔
911
                UpdatedCount: res.ModifiedCount,
13✔
912
        }, nil
13✔
913
}
914

915
func predicateToQuery(pred model.FilterPredicate) (bson.D, error) {
2✔
916
        if err := pred.Validate(); err != nil {
3✔
917
                return nil, err
1✔
918
        }
1✔
919
        name := fmt.Sprintf(
1✔
920
                "%s.%s-%s.value",
1✔
921
                DbDevAttributes,
1✔
922
                pred.Scope,
1✔
923
                model.GetDeviceAttributeNameReplacer().Replace(pred.Attribute),
1✔
924
        )
1✔
925
        return bson.D{{
1✔
926
                Key: name, Value: bson.D{{Key: pred.Type, Value: pred.Value}},
1✔
927
        }}, nil
1✔
928
}
929

930
func (db *DataStoreMongo) ListGroups(
931
        ctx context.Context,
932
        filters []model.FilterPredicate,
933
) ([]model.GroupName, error) {
12✔
934
        c := db.client.
12✔
935
                Database(mstore.DbFromContext(ctx, DbName)).
12✔
936
                Collection(DbDevicesColl)
12✔
937

12✔
938
        fltr := bson.D{{
12✔
939
                Key: DbDevAttributesGroupValue, Value: bson.M{"$exists": true},
12✔
940
        }}
12✔
941
        if len(fltr) > 0 {
24✔
942
                for _, p := range filters {
14✔
943
                        q, err := predicateToQuery(p)
2✔
944
                        if err != nil {
3✔
945
                                return nil, errors.Wrap(
1✔
946
                                        err, "store: bad filter predicate",
1✔
947
                                )
1✔
948
                        }
1✔
949
                        fltr = append(fltr, q...)
1✔
950
                }
951
        }
952
        results, err := c.Distinct(
11✔
953
                ctx, DbDevAttributesGroupValue, fltr,
11✔
954
        )
11✔
955
        if err != nil {
11✔
UNCOV
956
                return nil, err
×
UNCOV
957
        }
×
958

959
        groups := make([]model.GroupName, len(results))
11✔
960
        for i, d := range results {
47✔
961
                groups[i] = model.GroupName(d.(string))
36✔
962
        }
36✔
963
        return groups, nil
11✔
964
}
965

966
func (db *DataStoreMongo) GetDevicesByGroup(
967
        ctx context.Context,
968
        group model.GroupName,
969
        skip,
970
        limit int,
971
) ([]model.DeviceID, int, error) {
37✔
972
        c := db.client.
37✔
973
                Database(mstore.DbFromContext(ctx, DbName)).
37✔
974
                Collection(DbDevicesColl)
37✔
975

37✔
976
        filter := bson.M{DbDevAttributesGroupValue: group}
37✔
977
        result := c.FindOne(ctx, filter)
37✔
978
        if result == nil {
37✔
UNCOV
979
                return nil, -1, store.ErrGroupNotFound
×
UNCOV
980
        }
×
981

982
        var dev model.Device
37✔
983
        err := result.Decode(&dev)
37✔
984
        if err != nil {
43✔
985
                return nil, -1, store.ErrGroupNotFound
6✔
986
        }
6✔
987

988
        hasGroup := group != ""
31✔
989
        devices, totalDevices, e := db.GetDevices(ctx,
31✔
990
                store.ListQuery{
31✔
991
                        Skip:      skip,
31✔
992
                        Limit:     limit,
31✔
993
                        Filters:   nil,
31✔
994
                        Sort:      nil,
31✔
995
                        HasGroup:  &hasGroup,
31✔
996
                        GroupName: string(group)})
31✔
997
        if e != nil {
31✔
UNCOV
998
                return nil, -1, errors.Wrap(e, "failed to get device list for group")
×
UNCOV
999
        }
×
1000

1001
        resIds := make([]model.DeviceID, len(devices))
31✔
1002
        for i, d := range devices {
84✔
1003
                resIds[i] = d.ID
53✔
1004
        }
53✔
1005
        return resIds, totalDevices, nil
31✔
1006
}
1007

1008
func (db *DataStoreMongo) GetDeviceGroup(
1009
        ctx context.Context,
1010
        id model.DeviceID,
1011
) (model.GroupName, error) {
6✔
1012
        dev, err := db.GetDevice(ctx, id)
6✔
1013
        if err != nil || dev == nil {
8✔
1014
                return "", store.ErrDevNotFound
2✔
1015
        }
2✔
1016

1017
        return dev.Group, nil
4✔
1018
}
1019

1020
func (db *DataStoreMongo) DeleteDevices(
1021
        ctx context.Context, ids []model.DeviceID,
1022
) (*model.UpdateResult, error) {
3✔
1023
        var filter = bson.M{}
3✔
1024
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
3✔
1025
        collDevs := database.Collection(DbDevicesColl)
3✔
1026

3✔
1027
        switch len(ids) {
3✔
UNCOV
1028
        case 0:
×
UNCOV
1029
                // This is a no-op, don't bother requesting mongo.
×
UNCOV
1030
                return &model.UpdateResult{DeletedCount: 0}, nil
×
1031
        case 1:
2✔
1032
                filter[DbDevId] = ids[0]
2✔
1033
        default:
1✔
1034
                filter[DbDevId] = bson.M{"$in": ids}
1✔
1035
        }
1036
        res, err := collDevs.DeleteMany(ctx, filter)
3✔
1037
        if err != nil {
3✔
UNCOV
1038
                return nil, err
×
UNCOV
1039
        }
×
1040
        return &model.UpdateResult{
3✔
1041
                DeletedCount: res.DeletedCount,
3✔
1042
        }, nil
3✔
1043
}
1044

1045
func (db *DataStoreMongo) GetAllAttributeNames(ctx context.Context) ([]string, error) {
31✔
1046
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
31✔
1047

31✔
1048
        project := bson.M{
31✔
1049
                "$project": bson.M{
31✔
1050
                        "arrayofkeyvalue": bson.M{
31✔
1051
                                "$objectToArray": "$$ROOT.attributes",
31✔
1052
                        },
31✔
1053
                },
31✔
1054
        }
31✔
1055

31✔
1056
        unwind := bson.M{
31✔
1057
                "$unwind": "$arrayofkeyvalue",
31✔
1058
        }
31✔
1059

31✔
1060
        group := bson.M{
31✔
1061
                "$group": bson.M{
31✔
1062
                        "_id": nil,
31✔
1063
                        "allkeys": bson.M{
31✔
1064
                                "$addToSet": "$arrayofkeyvalue.v.name",
31✔
1065
                        },
31✔
1066
                },
31✔
1067
        }
31✔
1068

31✔
1069
        l := log.FromContext(ctx)
31✔
1070
        cursor, err := c.Aggregate(ctx, []bson.M{
31✔
1071
                project,
31✔
1072
                unwind,
31✔
1073
                group,
31✔
1074
        })
31✔
1075
        if err != nil {
31✔
UNCOV
1076
                return nil, err
×
UNCOV
1077
        }
×
1078
        defer cursor.Close(ctx)
31✔
1079

31✔
1080
        cursor.Next(ctx)
31✔
1081
        elem := &bson.D{}
31✔
1082
        err = cursor.Decode(elem)
31✔
1083
        if err != nil {
45✔
1084
                if err != io.EOF {
14✔
UNCOV
1085
                        return nil, errors.Wrap(err, "failed to get attributes")
×
1086
                } else {
14✔
1087
                        return make([]string, 0), nil
14✔
1088
                }
14✔
1089
        }
1090
        bsonValue, err := bson.Marshal(elem)
17✔
1091
        if err != nil {
17✔
UNCOV
1092
                return make([]string, 0), nil
×
UNCOV
1093
        }
×
1094
        var mapValue bson.M
17✔
1095
        err = bson.Unmarshal(bsonValue, &mapValue)
17✔
1096
        if err != nil {
17✔
UNCOV
1097
                return make([]string, 0), nil
×
UNCOV
1098
        }
×
1099
        results := mapValue["allkeys"].(primitive.A)
17✔
1100
        attributeNames := make([]string, len(results))
17✔
1101
        for i, d := range results {
79✔
1102
                attributeNames[i] = d.(string)
62✔
1103
                l.Debugf("GetAllAttributeNames got: '%v'", d)
62✔
1104
        }
62✔
1105

1106
        return attributeNames, nil
17✔
1107
}
1108

1109
func (db *DataStoreMongo) SearchDevices(
1110
        ctx context.Context,
1111
        searchParams model.SearchParams,
1112
) ([]model.Device, int, error) {
16✔
1113
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
16✔
1114

16✔
1115
        queryFilters := make([]bson.M, 0)
16✔
1116
        for _, filter := range searchParams.Filters {
30✔
1117
                op := filter.Type
14✔
1118
                var field string
14✔
1119
                if filter.Scope == model.AttrScopeIdentity && filter.Attribute == model.AttrNameID {
16✔
1120
                        field = DbDevId
2✔
1121
                } else {
14✔
1122
                        name := fmt.Sprintf(
12✔
1123
                                "%s-%s",
12✔
1124
                                filter.Scope,
12✔
1125
                                model.GetDeviceAttributeNameReplacer().Replace(filter.Attribute),
12✔
1126
                        )
12✔
1127
                        field = fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
12✔
1128
                }
12✔
1129
                queryFilters = append(queryFilters, bson.M{field: bson.M{op: filter.Value}})
14✔
1130
        }
1131

1132
        // FIXME: remove after migrating ids to attributes
1133
        if len(searchParams.DeviceIDs) > 0 {
17✔
1134
                queryFilters = append(queryFilters, bson.M{"_id": bson.M{"$in": searchParams.DeviceIDs}})
1✔
1135
        }
1✔
1136

1137
        if searchParams.Text != "" {
17✔
1138
                queryFilters = append(queryFilters, bson.M{
1✔
1139
                        "$text": bson.M{
1✔
1140
                                "$search": utils.TextToKeywords(searchParams.Text),
1✔
1141
                        },
1✔
1142
                })
1✔
1143
        }
1✔
1144

1145
        findQuery := bson.M{}
16✔
1146
        if len(queryFilters) > 0 {
29✔
1147
                findQuery["$and"] = queryFilters
13✔
1148
        }
13✔
1149

1150
        findOptions := mopts.Find()
16✔
1151
        findOptions.SetSkip(int64((searchParams.Page - 1) * searchParams.PerPage))
16✔
1152
        findOptions.SetLimit(int64(searchParams.PerPage))
16✔
1153

16✔
1154
        if len(searchParams.Attributes) > 0 {
18✔
1155
                name := fmt.Sprintf(
2✔
1156
                        "%s-%s",
2✔
1157
                        model.AttrScopeSystem,
2✔
1158
                        model.GetDeviceAttributeNameReplacer().Replace(DbDevUpdatedTs),
2✔
1159
                )
2✔
1160
                field := fmt.Sprintf("%s.%s", DbDevAttributes, name)
2✔
1161
                projection := bson.M{field: 1}
2✔
1162
                for _, attribute := range searchParams.Attributes {
5✔
1163
                        name := fmt.Sprintf(
3✔
1164
                                "%s-%s",
3✔
1165
                                attribute.Scope,
3✔
1166
                                model.GetDeviceAttributeNameReplacer().Replace(attribute.Attribute),
3✔
1167
                        )
3✔
1168
                        field := fmt.Sprintf("%s.%s", DbDevAttributes, name)
3✔
1169
                        projection[field] = 1
3✔
1170
                }
3✔
1171
                findOptions.SetProjection(projection)
2✔
1172
        }
1173

1174
        if searchParams.Text != "" {
17✔
1175
                findOptions.SetSort(bson.M{"score": bson.M{"$meta": "textScore"}})
1✔
1176
        } else if len(searchParams.Sort) > 0 {
21✔
1177
                sortField := make(bson.D, len(searchParams.Sort))
5✔
1178
                for i, sortQ := range searchParams.Sort {
11✔
1179
                        var field string
6✔
1180
                        if sortQ.Scope == model.AttrScopeIdentity && sortQ.Attribute == model.AttrNameID {
7✔
1181
                                field = DbDevId
1✔
1182
                        } else {
6✔
1183
                                name := fmt.Sprintf(
5✔
1184
                                        "%s-%s",
5✔
1185
                                        sortQ.Scope,
5✔
1186
                                        model.GetDeviceAttributeNameReplacer().Replace(sortQ.Attribute),
5✔
1187
                                )
5✔
1188
                                field = fmt.Sprintf("%s.%s.value", DbDevAttributes, name)
5✔
1189
                        }
5✔
1190
                        sortField[i] = bson.E{Key: field, Value: 1}
6✔
1191
                        if sortQ.Order == "desc" {
10✔
1192
                                sortField[i].Value = -1
4✔
1193
                        }
4✔
1194
                }
1195
                findOptions.SetSort(sortField)
5✔
1196
        }
1197

1198
        cursor, err := c.Find(ctx, findQuery, findOptions)
16✔
1199
        if err != nil {
17✔
1200
                return nil, -1, errors.Wrap(err, "failed to search devices")
1✔
1201
        }
1✔
1202
        defer cursor.Close(ctx)
15✔
1203

15✔
1204
        devices := []model.Device{}
15✔
1205

15✔
1206
        if err = cursor.All(ctx, &devices); err != nil {
15✔
UNCOV
1207
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
UNCOV
1208
        }
×
1209

1210
        count, err := c.CountDocuments(ctx, findQuery)
15✔
1211
        if err != nil {
15✔
UNCOV
1212
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
UNCOV
1213
        }
×
1214

1215
        return devices, int(count), nil
15✔
1216
}
1217

1218
func indexAttr(s *mongo.Client, ctx context.Context, attr string) error {
68✔
1219
        l := log.FromContext(ctx)
68✔
1220
        c := s.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
68✔
1221

68✔
1222
        indexView := c.Indexes()
68✔
1223
        keys := bson.D{
68✔
1224
                {Key: indexAttrName(attrIdentityStatus), Value: 1},
68✔
1225
                {Key: indexAttrName(attr), Value: 1},
68✔
1226
        }
68✔
1227
        _, err := indexView.CreateOne(ctx, mongo.IndexModel{Keys: keys, Options: &mopts.IndexOptions{
68✔
1228
                Name: &attr,
68✔
1229
        }})
68✔
1230

68✔
1231
        if err != nil {
68✔
1232
                if isTooManyIndexes(err) {
×
1233
                        l.Warnf(
×
1234
                                "failed to index attr %s in db %s: too many indexes",
×
1235
                                attr,
×
1236
                                mstore.DbFromContext(ctx, DbName),
×
1237
                        )
×
1238
                } else {
×
1239
                        return errors.Wrapf(
×
1240
                                err,
×
1241
                                "failed to index attr %s in db %s",
×
1242
                                attr,
×
UNCOV
1243
                                mstore.DbFromContext(ctx, DbName),
×
UNCOV
1244
                        )
×
UNCOV
1245
                }
×
1246
        }
1247

1248
        return nil
68✔
1249
}
1250

1251
func indexAttrName(attr string) string {
152✔
1252
        return fmt.Sprintf("attributes.%s.value", attr)
152✔
1253
}
152✔
1254

UNCOV
1255
func isTooManyIndexes(e error) bool {
×
UNCOV
1256
        return strings.HasPrefix(e.Error(), "add index fails, too many indexes for inventory.devices")
×
UNCOV
1257
}
×
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