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

mendersoftware / mender-server / 1622978334

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

Pull #300

gitlab-ci

alfrunes
fix: Deployment device count should not exceed max devices

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

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

4251 of 6164 branches covered (68.96%)

Branch coverage included in aggregate %.

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

2544 existing lines in 83 files now uncovered.

42741 of 58384 relevant lines covered (73.21%)

21.49 hits per line

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

89.95
/backend/services/inventory/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/mender-server/pkg/log"
35
        mstore "github.com/mendersoftware/mender-server/pkg/store"
36

37
        "github.com/mendersoftware/mender-server/services/inventory/model"
38
        "github.com/mendersoftware/mender-server/services/inventory/store"
39
        "github.com/mendersoftware/mender-server/services/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
        FiltersAttributesMaxDevices = 5000
67
        FiltersAttributesLimit      = 500
68

69
        attrIdentityStatus = "identity-status"
70
)
71

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
162
        return db, nil
1✔
163
}
164

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

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

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

219
        findQuery := bson.M{}
1✔
220
        if len(queryFilters) > 0 {
2✔
221
                findQuery["$and"] = queryFilters
1✔
222
        }
1✔
223

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

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

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

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

261
        return devices, int(count), nil
1✔
262
}
263

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

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

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

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

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

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

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

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

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

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

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

381
        const updatedField = systemScope + "-" + model.AttrNameUpdated
1✔
382
        if withUpdated {
2✔
383
                update[updatedField] = model.DeviceAttribute{
1✔
384
                        Scope: model.AttrScopeSystem,
1✔
385
                        Name:  model.AttrNameUpdated,
1✔
386
                        Value: now,
1✔
387
                }
1✔
388
        }
1✔
389

390
        switch len(devices) {
1✔
391
        case 0:
1✔
392
                return &model.UpdateResult{}, nil
1✔
393
        case 1:
1✔
394
                filter := bson.M{
1✔
395
                        "_id": devices[0].Id,
1✔
396
                }
1✔
397
                updateOpts := mopts.FindOneAndUpdate().
1✔
398
                        SetUpsert(true).
1✔
399
                        SetReturnDocument(mopts.After)
1✔
400

1✔
401
                if withRevision {
2✔
402
                        filter[DbDevRevision] = bson.M{"$lt": devices[0].Revision}
1✔
403
                        update[DbDevRevision] = devices[0].Revision
1✔
404
                }
1✔
405
                if scope == model.AttrScopeTags {
2✔
406
                        update[etagField] = uuid.New().String()
1✔
407
                        updateOpts = mopts.FindOneAndUpdate().
1✔
408
                                SetUpsert(false).
1✔
409
                                SetReturnDocument(mopts.After)
1✔
410
                }
1✔
411
                if etag != "" {
1✔
UNCOV
412
                        filter[etagField] = bson.M{"$eq": etag}
×
UNCOV
413
                }
×
414

415
                update = bson.M{
1✔
416
                        "$set":         update,
1✔
417
                        "$setOnInsert": oninsert,
1✔
418
                }
1✔
419

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

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

488
// makeAttrField is a convenience function for composing attribute field names.
489
func makeAttrField(attrName, attrScope string, subFields ...string) string {
1✔
490
        field := fmt.Sprintf(
1✔
491
                "%s.%s-%s",
1✔
492
                DbDevAttributes,
1✔
493
                attrScope,
1✔
494
                model.GetDeviceAttributeNameReplacer().Replace(attrName),
1✔
495
        )
1✔
496
        if len(subFields) > 0 {
2✔
497
                field = strings.Join(
1✔
498
                        append([]string{field}, subFields...), ".",
1✔
499
                )
1✔
500
        }
1✔
501
        return field
1✔
502
}
503

504
// makeAttrUpsert creates a new upsert document for the given attributes.
505
func makeAttrUpsert(attrs model.DeviceAttributes) (bson.M, error) {
1✔
506
        var fieldName string
1✔
507
        upsert := make(bson.M)
1✔
508

1✔
509
        for i := range attrs {
2✔
510
                if attrs[i].Name == "" {
2✔
511
                        return nil, store.ErrNoAttrName
1✔
512
                }
1✔
513
                if attrs[i].Scope == "" {
2✔
514
                        // Default to inventory scope
1✔
515
                        attrs[i].Scope = model.AttrScopeInventory
1✔
516
                }
1✔
517

518
                fieldName = makeAttrField(
1✔
519
                        attrs[i].Name,
1✔
520
                        attrs[i].Scope,
1✔
521
                        DbDevAttributesScope,
1✔
522
                )
1✔
523
                upsert[fieldName] = attrs[i].Scope
1✔
524

1✔
525
                fieldName = makeAttrField(
1✔
526
                        attrs[i].Name,
1✔
527
                        attrs[i].Scope,
1✔
528
                        DbDevAttributesName,
1✔
529
                )
1✔
530
                upsert[fieldName] = attrs[i].Name
1✔
531

1✔
532
                if attrs[i].Value != nil {
2✔
533
                        fieldName = makeAttrField(
1✔
534
                                attrs[i].Name,
1✔
535
                                attrs[i].Scope,
1✔
536
                                DbDevAttributesValue,
1✔
537
                        )
1✔
538
                        upsert[fieldName] = attrs[i].Value
1✔
539
                }
1✔
540

541
                if attrs[i].Description != nil {
2✔
542
                        fieldName = makeAttrField(
1✔
543
                                attrs[i].Name,
1✔
544
                                attrs[i].Scope,
1✔
545
                                DbDevAttributesDesc,
1✔
546
                        )
1✔
547
                        upsert[fieldName] = attrs[i].Description
1✔
548
                }
1✔
549

550
                if attrs[i].Timestamp != nil {
2✔
551
                        fieldName = makeAttrField(
1✔
552
                                attrs[i].Name,
1✔
553
                                attrs[i].Scope,
1✔
554
                                DbDevAttributesTs,
1✔
555
                        )
1✔
556
                        upsert[fieldName] = attrs[i].Timestamp
1✔
557
                }
1✔
558
        }
559
        return upsert, nil
1✔
560
}
561

562
// makeAttrRemove creates a new unset document to remove attributes
563
func makeAttrRemove(attrs model.DeviceAttributes) (bson.M, error) {
1✔
564
        var fieldName string
1✔
565
        remove := make(bson.M)
1✔
566

1✔
567
        for i := range attrs {
2✔
568
                if attrs[i].Name == "" {
1✔
569
                        return nil, store.ErrNoAttrName
×
570
                }
×
571
                if attrs[i].Scope == "" {
1✔
572
                        // Default to inventory scope
×
573
                        attrs[i].Scope = model.AttrScopeInventory
×
574
                }
×
575
                fieldName = makeAttrField(
1✔
576
                        attrs[i].Name,
1✔
577
                        attrs[i].Scope,
1✔
578
                )
1✔
579
                remove[fieldName] = true
1✔
580
        }
581
        return remove, nil
1✔
582
}
583

584
func mongoOperator(co store.ComparisonOperator) string {
1✔
585
        switch co {
1✔
586
        case store.Eq:
1✔
587
                return "$eq"
1✔
588
        }
589
        return ""
×
590
}
591

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

1✔
608
        c := db.client.
1✔
609
                Database(mstore.DbFromContext(ctx, DbName)).
1✔
610
                Collection(DbDevicesColl)
1✔
611

1✔
612
        update, err := makeAttrUpsert(updateAttrs)
1✔
613
        if err != nil {
2✔
614
                return nil, err
1✔
615
        }
1✔
616
        remove, err := makeAttrRemove(removeAttrs)
1✔
617
        if err != nil {
1✔
618
                return nil, err
×
619
        }
×
620
        filter := bson.M{"_id": id}
1✔
621
        if etag != "" {
2✔
622
                filter[etagField] = bson.M{"$eq": etag}
1✔
623
        }
1✔
624

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

654
        device := &model.Device{}
1✔
655
        res := c.FindOneAndUpdate(ctx, filter, update, updateOpts)
1✔
656
        err = res.Decode(device)
1✔
657
        if err == mongo.ErrNoDocuments {
2✔
658
                return &model.UpdateResult{
1✔
659
                        MatchedCount: 0,
1✔
660
                        CreatedCount: 0,
1✔
661
                        Devices:      []*model.Device{},
1✔
662
                }, nil
1✔
663
        } else if err == nil {
3✔
664
                return &model.UpdateResult{
1✔
665
                        MatchedCount: 1,
1✔
666
                        CreatedCount: 0,
1✔
667
                        Devices:      []*model.Device{device},
1✔
668
                }, nil
1✔
669
        }
1✔
670
        return nil, err
×
671
}
672

673
func (db *DataStoreMongo) UpdateDevicesGroup(
674
        ctx context.Context,
675
        devIDs []model.DeviceID,
676
        group model.GroupName,
677
) (*model.UpdateResult, error) {
1✔
678
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
679
        collDevs := database.Collection(DbDevicesColl)
1✔
680

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

709
// UpdateDeviceText updates the device text field
710
func (db *DataStoreMongo) UpdateDeviceText(
711
        ctx context.Context,
712
        deviceID model.DeviceID,
713
        text string,
714
) error {
1✔
715
        filter := bson.M{
1✔
716
                DbDevId: deviceID.String(),
1✔
717
        }
1✔
718

1✔
719
        update := bson.M{
1✔
720
                "$set": bson.M{
1✔
721
                        DbDevAttributesText: text,
1✔
722
                },
1✔
723
        }
1✔
724

1✔
725
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
726
        collDevs := database.Collection(DbDevicesColl)
1✔
727

1✔
728
        _, err := collDevs.UpdateOne(ctx, filter, update)
1✔
729
        return err
1✔
730
}
1✔
731

732
func (db *DataStoreMongo) GetFiltersAttributes(
733
        ctx context.Context,
734
) ([]model.FilterAttribute, error) {
1✔
735
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
736
        collDevs := database.Collection(DbDevicesColl)
1✔
737

1✔
738
        const DbCount = "count"
1✔
739

1✔
740
        cur, err := collDevs.Aggregate(ctx, []bson.M{
1✔
741
                // Sample up to 5,000 devices to get a representative sample
1✔
742
                {
1✔
743
                        "$limit": FiltersAttributesMaxDevices,
1✔
744
                },
1✔
745
                {
1✔
746
                        "$project": bson.M{
1✔
747
                                "_id": 0,
1✔
748
                                "attributes": bson.M{
1✔
749
                                        "$objectToArray": "$" + DbDevAttributes,
1✔
750
                                },
1✔
751
                        },
1✔
752
                },
1✔
753
                {
1✔
754
                        "$unwind": "$" + DbDevAttributes,
1✔
755
                },
1✔
756
                {
1✔
757
                        "$group": bson.M{
1✔
758
                                DbDevId: bson.M{
1✔
759
                                        DbDevAttributesName:  "$" + DbDevAttributes + ".v." + DbDevAttributesName,
1✔
760
                                        DbDevAttributesScope: "$" + DbDevAttributes + ".v." + DbDevAttributesScope,
1✔
761
                                },
1✔
762
                                DbCount: bson.M{
1✔
763
                                        "$sum": 1,
1✔
764
                                },
1✔
765
                        },
1✔
766
                },
1✔
767
                {
1✔
768
                        "$limit": FiltersAttributesLimit,
1✔
769
                },
1✔
770
                {
1✔
771
                        "$sort": bson.D{
1✔
772
                                {Key: DbCount, Value: -1},
1✔
773
                                {Key: DbDevId + "." + DbDevAttributesScope, Value: 1},
1✔
774
                                {Key: DbDevId + "." + DbDevAttributesName, Value: 1},
1✔
775
                        },
1✔
776
                },
1✔
777
        })
1✔
778
        if err != nil {
1✔
779
                return nil, err
×
780
        }
×
781
        defer cur.Close(ctx)
1✔
782

1✔
783
        var attributes []model.FilterAttribute
1✔
784
        type Result struct {
1✔
785
                Group struct {
1✔
786
                        Name  string `bson:"name"`
1✔
787
                        Scope string `bson:"scope"`
1✔
788
                } `bson:"_id"`
1✔
789
                Count int32 `bson:"count"`
1✔
790
        }
1✔
791
        for cur.Next(ctx) {
2✔
792
                var elem Result
1✔
793
                err = cur.Decode(&elem)
1✔
794
                if err != nil {
1✔
795
                        break
×
796
                }
797
                attributes = append(attributes, model.FilterAttribute{
1✔
798
                        Name:  elem.Group.Name,
1✔
799
                        Scope: elem.Group.Scope,
1✔
800
                        Count: elem.Count,
1✔
801
                })
1✔
802
        }
803

804
        return attributes, nil
1✔
805
}
806

807
func (db *DataStoreMongo) DeleteGroup(
808
        ctx context.Context,
809
        group model.GroupName,
810
) (chan model.DeviceID, error) {
1✔
811
        deviceIDs := make(chan model.DeviceID)
1✔
812

1✔
813
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
814
        collDevs := database.Collection(DbDevicesColl)
1✔
815

1✔
816
        filter := bson.M{DbDevAttributesGroupValue: group}
1✔
817

1✔
818
        const batchMaxSize = 100
1✔
819
        batchSize := int32(batchMaxSize)
1✔
820
        findOptions := &mopts.FindOptions{
1✔
821
                Projection: bson.M{DbDevId: 1},
1✔
822
                BatchSize:  &batchSize,
1✔
823
        }
1✔
824
        cursor, err := collDevs.Find(ctx, filter, findOptions)
1✔
825
        if err != nil {
1✔
826
                return nil, err
×
827
        }
×
828

829
        go func() {
2✔
830
                defer cursor.Close(ctx)
1✔
831
                batch := make([]model.DeviceID, batchMaxSize)
1✔
832
                batchSize := 0
1✔
833

1✔
834
                update := bson.M{"$unset": bson.M{DbDevAttributesGroup: 1}}
1✔
835
                device := &model.Device{}
1✔
836
                defer close(deviceIDs)
1✔
837

1✔
838
        next:
1✔
839
                for {
2✔
840
                        hasNext := cursor.Next(ctx)
1✔
841
                        if !hasNext {
2✔
842
                                if batchSize > 0 {
2✔
843
                                        break
1✔
844
                                }
845
                                return
1✔
846
                        }
847
                        if err = cursor.Decode(&device); err == nil {
2✔
848
                                batch[batchSize] = device.ID
1✔
849
                                batchSize++
1✔
850
                                if len(batch) == batchSize {
1✔
851
                                        break
×
852
                                }
853
                        }
854
                }
855

856
                _, _ = collDevs.UpdateMany(ctx, bson.M{DbDevId: bson.M{"$in": batch[:batchSize]}}, update)
1✔
857
                for _, item := range batch[:batchSize] {
2✔
858
                        deviceIDs <- item
1✔
859
                }
1✔
860
                batchSize = 0
1✔
861
                goto next
1✔
862
        }()
863

864
        return deviceIDs, nil
1✔
865
}
866

867
func (db *DataStoreMongo) UnsetDevicesGroup(
868
        ctx context.Context,
869
        deviceIDs []model.DeviceID,
870
        group model.GroupName,
871
) (*model.UpdateResult, error) {
1✔
872
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
873
        collDevs := database.Collection(DbDevicesColl)
1✔
874

1✔
875
        var filter bson.D
1✔
876
        // Add filter on device id (either $in or direct indexing)
1✔
877
        switch len(deviceIDs) {
1✔
878
        case 0:
1✔
879
                return &model.UpdateResult{}, nil
1✔
880
        case 1:
1✔
881
                filter = bson.D{{Key: DbDevId, Value: deviceIDs[0]}}
1✔
882
        default:
1✔
883
                filter = bson.D{{Key: DbDevId, Value: bson.M{"$in": deviceIDs}}}
1✔
884
        }
885
        // Append filter on group
886
        filter = append(
1✔
887
                filter,
1✔
888
                bson.E{Key: DbDevAttributesGroupValue, Value: group},
1✔
889
        )
1✔
890
        // Create unset operation on group attribute
1✔
891
        update := bson.M{
1✔
892
                "$unset": bson.M{
1✔
893
                        DbDevAttributesGroup: "",
1✔
894
                },
1✔
895
        }
1✔
896
        res, err := collDevs.UpdateMany(ctx, filter, update)
1✔
897
        if err != nil {
1✔
898
                return nil, err
×
899
        }
×
900
        return &model.UpdateResult{
1✔
901
                MatchedCount: res.MatchedCount,
1✔
902
                UpdatedCount: res.ModifiedCount,
1✔
903
        }, nil
1✔
904
}
905

906
func predicateToQuery(pred model.FilterPredicate) (bson.D, error) {
1✔
907
        if err := pred.Validate(); err != nil {
2✔
908
                return nil, err
1✔
909
        }
1✔
910
        name := fmt.Sprintf(
1✔
911
                "%s.%s-%s.value",
1✔
912
                DbDevAttributes,
1✔
913
                pred.Scope,
1✔
914
                model.GetDeviceAttributeNameReplacer().Replace(pred.Attribute),
1✔
915
        )
1✔
916
        return bson.D{{
1✔
917
                Key: name, Value: bson.D{{Key: pred.Type, Value: pred.Value}},
1✔
918
        }}, nil
1✔
919
}
920

921
func (db *DataStoreMongo) ListGroups(
922
        ctx context.Context,
923
        filters []model.FilterPredicate,
924
) ([]model.GroupName, error) {
1✔
925
        c := db.client.
1✔
926
                Database(mstore.DbFromContext(ctx, DbName)).
1✔
927
                Collection(DbDevicesColl)
1✔
928

1✔
929
        fltr := bson.D{{
1✔
930
                Key: DbDevAttributesGroupValue, Value: bson.M{"$exists": true},
1✔
931
        }}
1✔
932
        if len(fltr) > 0 {
2✔
933
                for _, p := range filters {
2✔
934
                        q, err := predicateToQuery(p)
1✔
935
                        if err != nil {
2✔
936
                                return nil, errors.Wrap(
1✔
937
                                        err, "store: bad filter predicate",
1✔
938
                                )
1✔
939
                        }
1✔
940
                        fltr = append(fltr, q...)
1✔
941
                }
942
        }
943
        results, err := c.Distinct(
1✔
944
                ctx, DbDevAttributesGroupValue, fltr,
1✔
945
        )
1✔
946
        if err != nil {
1✔
947
                return nil, err
×
948
        }
×
949

950
        groups := make([]model.GroupName, len(results))
1✔
951
        for i, d := range results {
2✔
952
                groups[i] = model.GroupName(d.(string))
1✔
953
        }
1✔
954
        return groups, nil
1✔
955
}
956

957
func (db *DataStoreMongo) GetDevicesByGroup(
958
        ctx context.Context,
959
        group model.GroupName,
960
        skip,
961
        limit int,
962
) ([]model.DeviceID, int, error) {
1✔
963
        c := db.client.
1✔
964
                Database(mstore.DbFromContext(ctx, DbName)).
1✔
965
                Collection(DbDevicesColl)
1✔
966

1✔
967
        filter := bson.M{DbDevAttributesGroupValue: group}
1✔
968
        result := c.FindOne(ctx, filter)
1✔
969
        if result == nil {
1✔
970
                return nil, -1, store.ErrGroupNotFound
×
971
        }
×
972

973
        var dev model.Device
1✔
974
        err := result.Decode(&dev)
1✔
975
        if err != nil {
2✔
976
                return nil, -1, store.ErrGroupNotFound
1✔
977
        }
1✔
978

979
        hasGroup := group != ""
1✔
980
        devices, totalDevices, e := db.GetDevices(ctx,
1✔
981
                store.ListQuery{
1✔
982
                        Skip:      skip,
1✔
983
                        Limit:     limit,
1✔
984
                        Filters:   nil,
1✔
985
                        Sort:      nil,
1✔
986
                        HasGroup:  &hasGroup,
1✔
987
                        GroupName: string(group)})
1✔
988
        if e != nil {
1✔
989
                return nil, -1, errors.Wrap(e, "failed to get device list for group")
×
990
        }
×
991

992
        resIds := make([]model.DeviceID, len(devices))
1✔
993
        for i, d := range devices {
2✔
994
                resIds[i] = d.ID
1✔
995
        }
1✔
996
        return resIds, totalDevices, nil
1✔
997
}
998

999
func (db *DataStoreMongo) GetDeviceGroup(
1000
        ctx context.Context,
1001
        id model.DeviceID,
1002
) (model.GroupName, error) {
1✔
1003
        dev, err := db.GetDevice(ctx, id)
1✔
1004
        if err != nil || dev == nil {
2✔
1005
                return "", store.ErrDevNotFound
1✔
1006
        }
1✔
1007

1008
        return dev.Group, nil
1✔
1009
}
1010

1011
func (db *DataStoreMongo) DeleteDevices(
1012
        ctx context.Context, ids []model.DeviceID,
1013
) (*model.UpdateResult, error) {
1✔
1014
        var filter = bson.M{}
1✔
1015
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
1✔
1016
        collDevs := database.Collection(DbDevicesColl)
1✔
1017

1✔
1018
        switch len(ids) {
1✔
1019
        case 0:
×
1020
                // This is a no-op, don't bother requesting mongo.
×
1021
                return &model.UpdateResult{DeletedCount: 0}, nil
×
1022
        case 1:
1✔
1023
                filter[DbDevId] = ids[0]
1✔
1024
        default:
1✔
1025
                filter[DbDevId] = bson.M{"$in": ids}
1✔
1026
        }
1027
        res, err := collDevs.DeleteMany(ctx, filter)
1✔
1028
        if err != nil {
1✔
1029
                return nil, err
×
1030
        }
×
1031
        return &model.UpdateResult{
1✔
1032
                DeletedCount: res.DeletedCount,
1✔
1033
        }, nil
1✔
1034
}
1035

1036
func (db *DataStoreMongo) GetAllAttributeNames(ctx context.Context) ([]string, error) {
2✔
1037
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
2✔
1038

2✔
1039
        project := bson.M{
2✔
1040
                "$project": bson.M{
2✔
1041
                        "arrayofkeyvalue": bson.M{
2✔
1042
                                "$objectToArray": "$$ROOT.attributes",
2✔
1043
                        },
2✔
1044
                },
2✔
1045
        }
2✔
1046

2✔
1047
        unwind := bson.M{
2✔
1048
                "$unwind": "$arrayofkeyvalue",
2✔
1049
        }
2✔
1050

2✔
1051
        group := bson.M{
2✔
1052
                "$group": bson.M{
2✔
1053
                        "_id": nil,
2✔
1054
                        "allkeys": bson.M{
2✔
1055
                                "$addToSet": "$arrayofkeyvalue.v.name",
2✔
1056
                        },
2✔
1057
                },
2✔
1058
        }
2✔
1059

2✔
1060
        l := log.FromContext(ctx)
2✔
1061
        cursor, err := c.Aggregate(ctx, []bson.M{
2✔
1062
                project,
2✔
1063
                unwind,
2✔
1064
                group,
2✔
1065
        })
2✔
1066
        if err != nil {
2✔
1067
                return nil, err
×
1068
        }
×
1069
        defer cursor.Close(ctx)
2✔
1070

2✔
1071
        cursor.Next(ctx)
2✔
1072
        elem := &bson.D{}
2✔
1073
        err = cursor.Decode(elem)
2✔
1074
        if err != nil {
4✔
1075
                if err != io.EOF {
2✔
1076
                        return nil, errors.Wrap(err, "failed to get attributes")
×
1077
                } else {
2✔
1078
                        return make([]string, 0), nil
2✔
1079
                }
2✔
1080
        }
1081
        bsonValue, err := bson.Marshal(elem)
1✔
1082
        if err != nil {
1✔
1083
                return make([]string, 0), nil
×
1084
        }
×
1085
        var mapValue bson.M
1✔
1086
        err = bson.Unmarshal(bsonValue, &mapValue)
1✔
1087
        if err != nil {
1✔
1088
                return make([]string, 0), nil
×
1089
        }
×
1090
        results := mapValue["allkeys"].(primitive.A)
1✔
1091
        attributeNames := make([]string, len(results))
1✔
1092
        for i, d := range results {
2✔
1093
                attributeNames[i] = d.(string)
1✔
1094
                l.Debugf("GetAllAttributeNames got: '%v'", d)
1✔
1095
        }
1✔
1096

1097
        return attributeNames, nil
1✔
1098
}
1099

1100
func (db *DataStoreMongo) SearchDevices(
1101
        ctx context.Context,
1102
        searchParams model.SearchParams,
1103
) ([]model.Device, int, error) {
1✔
1104
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
1✔
1105

1✔
1106
        queryFilters := make([]bson.M, 0)
1✔
1107
        for _, filter := range searchParams.Filters {
2✔
1108
                op := filter.Type
1✔
1109
                var field string
1✔
1110
                if filter.Scope == model.AttrScopeIdentity && filter.Attribute == model.AttrNameID {
2✔
1111
                        field = DbDevId
1✔
1112
                } else {
2✔
1113
                        name := fmt.Sprintf(
1✔
1114
                                "%s-%s",
1✔
1115
                                filter.Scope,
1✔
1116
                                model.GetDeviceAttributeNameReplacer().Replace(filter.Attribute),
1✔
1117
                        )
1✔
1118
                        field = fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
1✔
1119
                }
1✔
1120
                queryFilters = append(queryFilters, bson.M{field: bson.M{op: filter.Value}})
1✔
1121
        }
1122

1123
        // FIXME: remove after migrating ids to attributes
1124
        if len(searchParams.DeviceIDs) > 0 {
2✔
1125
                queryFilters = append(queryFilters, bson.M{"_id": bson.M{"$in": searchParams.DeviceIDs}})
1✔
1126
        }
1✔
1127

1128
        if searchParams.Text != "" {
2✔
1129
                queryFilters = append(queryFilters, bson.M{
1✔
1130
                        "$text": bson.M{
1✔
1131
                                "$search": utils.TextToKeywords(searchParams.Text),
1✔
1132
                        },
1✔
1133
                })
1✔
1134
        }
1✔
1135

1136
        findQuery := bson.M{}
1✔
1137
        if len(queryFilters) > 0 {
2✔
1138
                findQuery["$and"] = queryFilters
1✔
1139
        }
1✔
1140

1141
        findOptions := mopts.Find()
1✔
1142
        findOptions.SetSkip(int64((searchParams.Page - 1) * searchParams.PerPage))
1✔
1143
        findOptions.SetLimit(int64(searchParams.PerPage))
1✔
1144

1✔
1145
        if len(searchParams.Attributes) > 0 {
2✔
1146
                name := fmt.Sprintf(
1✔
1147
                        "%s-%s",
1✔
1148
                        model.AttrScopeSystem,
1✔
1149
                        model.GetDeviceAttributeNameReplacer().Replace(DbDevUpdatedTs),
1✔
1150
                )
1✔
1151
                field := fmt.Sprintf("%s.%s", DbDevAttributes, name)
1✔
1152
                projection := bson.M{field: 1}
1✔
1153
                for _, attribute := range searchParams.Attributes {
2✔
1154
                        name := fmt.Sprintf(
1✔
1155
                                "%s-%s",
1✔
1156
                                attribute.Scope,
1✔
1157
                                model.GetDeviceAttributeNameReplacer().Replace(attribute.Attribute),
1✔
1158
                        )
1✔
1159
                        field := fmt.Sprintf("%s.%s", DbDevAttributes, name)
1✔
1160
                        projection[field] = 1
1✔
1161
                }
1✔
1162
                findOptions.SetProjection(projection)
1✔
1163
        }
1164

1165
        if searchParams.Text != "" {
2✔
1166
                findOptions.SetSort(bson.M{"score": bson.M{"$meta": "textScore"}})
1✔
1167
        } else if len(searchParams.Sort) > 0 {
3✔
1168
                sortField := make(bson.D, len(searchParams.Sort))
1✔
1169
                for i, sortQ := range searchParams.Sort {
2✔
1170
                        var field string
1✔
1171
                        if sortQ.Scope == model.AttrScopeIdentity && sortQ.Attribute == model.AttrNameID {
2✔
1172
                                field = DbDevId
1✔
1173
                        } else {
2✔
1174
                                name := fmt.Sprintf(
1✔
1175
                                        "%s-%s",
1✔
1176
                                        sortQ.Scope,
1✔
1177
                                        model.GetDeviceAttributeNameReplacer().Replace(sortQ.Attribute),
1✔
1178
                                )
1✔
1179
                                field = fmt.Sprintf("%s.%s.value", DbDevAttributes, name)
1✔
1180
                        }
1✔
1181
                        sortField[i] = bson.E{Key: field, Value: 1}
1✔
1182
                        if sortQ.Order == "desc" {
2✔
1183
                                sortField[i].Value = -1
1✔
1184
                        }
1✔
1185
                }
1186
                findOptions.SetSort(sortField)
1✔
1187
        }
1188

1189
        cursor, err := c.Find(ctx, findQuery, findOptions)
1✔
1190
        if err != nil {
2✔
1191
                return nil, -1, errors.Wrap(err, "failed to search devices")
1✔
1192
        }
1✔
1193
        defer cursor.Close(ctx)
1✔
1194

1✔
1195
        devices := []model.Device{}
1✔
1196

1✔
1197
        if err = cursor.All(ctx, &devices); err != nil {
1✔
1198
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
1199
        }
×
1200

1201
        count, err := c.CountDocuments(ctx, findQuery)
1✔
1202
        if err != nil {
1✔
1203
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
1204
        }
×
1205

1206
        return devices, int(count), nil
1✔
1207
}
1208

1209
func indexAttr(s *mongo.Client, ctx context.Context, attr string) error {
2✔
1210
        l := log.FromContext(ctx)
2✔
1211
        c := s.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
2✔
1212

2✔
1213
        indexView := c.Indexes()
2✔
1214
        keys := bson.D{
2✔
1215
                {Key: indexAttrName(attrIdentityStatus), Value: 1},
2✔
1216
                {Key: indexAttrName(attr), Value: 1},
2✔
1217
        }
2✔
1218
        _, err := indexView.CreateOne(ctx, mongo.IndexModel{Keys: keys, Options: &mopts.IndexOptions{
2✔
1219
                Name: &attr,
2✔
1220
        }})
2✔
1221

2✔
1222
        if err != nil {
2✔
1223
                if isTooManyIndexes(err) {
×
1224
                        l.Warnf(
×
1225
                                "failed to index attr %s in db %s: too many indexes",
×
1226
                                attr,
×
1227
                                mstore.DbFromContext(ctx, DbName),
×
1228
                        )
×
1229
                } else {
×
1230
                        return errors.Wrapf(
×
1231
                                err,
×
1232
                                "failed to index attr %s in db %s",
×
1233
                                attr,
×
1234
                                mstore.DbFromContext(ctx, DbName),
×
1235
                        )
×
1236
                }
×
1237
        }
1238

1239
        return nil
2✔
1240
}
1241

1242
func indexAttrName(attr string) string {
2✔
1243
        return fmt.Sprintf("attributes.%s.value", attr)
2✔
1244
}
2✔
1245

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