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

mendersoftware / iot-manager / 1401702106

05 Aug 2024 08:32PM UTC coverage: 87.577%. Remained the same
1401702106

push

gitlab-ci

web-flow
Merge pull request #295 from mendersoftware/dependabot/docker/docker-dependencies-03b04ac819

chore: bump golang from 1.22.4-alpine3.19 to 1.22.5-alpine3.19 in the docker-dependencies group

3264 of 3727 relevant lines covered (87.58%)

11.44 hits per line

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

84.83
/store/mongo/datastore_mongo.go
1
// Copyright 2024 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
        "strings"
22
        "time"
23

24
        "github.com/google/uuid"
25
        "github.com/pkg/errors"
26
        "go.mongodb.org/mongo-driver/bson"
27
        "go.mongodb.org/mongo-driver/mongo"
28
        mopts "go.mongodb.org/mongo-driver/mongo/options"
29

30
        "github.com/mendersoftware/go-lib-micro/config"
31
        "github.com/mendersoftware/go-lib-micro/identity"
32
        mstore "github.com/mendersoftware/go-lib-micro/store/v2"
33

34
        dconfig "github.com/mendersoftware/iot-manager/config"
35
        "github.com/mendersoftware/iot-manager/model"
36
        "github.com/mendersoftware/iot-manager/store"
37
)
38

39
const (
40
        CollNameDevices      = "devices"
41
        CollNameIntegrations = "integrations"
42

43
        KeyID             = "_id"
44
        KeyIntegrationIDs = "integration_ids"
45
        KeyProvider       = "provider"
46
        KeyTenantID       = "tenant_id"
47
        KeyCredentials    = "credentials"
48

49
        ConnectTimeoutSeconds = 10
50
        defaultAutomigrate    = false
51
)
52

53
var (
54
        ErrFailedToGetIntegrations = errors.New("failed to get integrations")
55
        ErrFailedToGetDevice       = errors.New("failed to get device")
56
        ErrFailedToGetSettings     = errors.New("failed to get settings")
57
)
58

59
type Config struct {
60
        Automigrate *bool
61
        DbName      *string
62
}
63

64
func NewConfig() *Config {
85✔
65
        conf := new(Config)
85✔
66
        return conf.
85✔
67
                SetAutomigrate(defaultAutomigrate).
85✔
68
                SetDbName(DbName)
85✔
69
}
85✔
70

71
func (c *Config) SetAutomigrate(migrate bool) *Config {
130✔
72
        c.Automigrate = &migrate
130✔
73
        return c
130✔
74
}
130✔
75

76
func (c *Config) SetDbName(name string) *Config {
123✔
77
        c.DbName = &name
123✔
78
        return c
123✔
79
}
123✔
80

81
func mergeConfig(configs ...*Config) *Config {
44✔
82
        config := NewConfig()
44✔
83
        for _, c := range configs {
88✔
84
                if c == nil {
44✔
85
                        continue
×
86
                }
87
                if c.Automigrate != nil {
88✔
88
                        config.SetAutomigrate(*c.Automigrate)
44✔
89
                }
44✔
90
                if c.DbName != nil {
88✔
91
                        config.DbName = c.DbName
44✔
92
                }
44✔
93
        }
94
        return config
44✔
95
}
96

97
// SetupDataStore returns the mongo data store and optionally runs migrations
98
func SetupDataStore(conf *Config) (store.DataStore, error) {
3✔
99
        conf = mergeConfig(conf)
3✔
100
        ctx := context.Background()
3✔
101
        dbClient, err := NewClient(ctx, config.Config)
3✔
102
        if err != nil {
3✔
103
                return nil, errors.New(fmt.Sprintf("failed to connect to db: %v", err))
×
104
        }
×
105
        dataStore := NewDataStoreWithClient(dbClient, conf)
3✔
106

3✔
107
        return dataStore, dataStore.Migrate(ctx)
3✔
108
}
109

110
func (ds *DataStoreMongo) Migrate(ctx context.Context) error {
3✔
111
        return Migrate(ctx, *ds.DbName, DbVersion, ds.client, *ds.Automigrate)
3✔
112
}
3✔
113

114
// NewClient returns a mongo client
115
func NewClient(ctx context.Context, c config.Reader) (*mongo.Client, error) {
3✔
116

3✔
117
        clientOptions := mopts.Client()
3✔
118
        mongoURL := c.GetString(dconfig.SettingMongo)
3✔
119
        if !strings.Contains(mongoURL, "://") {
3✔
120
                return nil, errors.Errorf("Invalid mongoURL %q: missing schema.",
×
121
                        mongoURL)
×
122
        }
×
123
        clientOptions.ApplyURI(mongoURL).SetRegistry(newRegistry())
3✔
124

3✔
125
        username := c.GetString(dconfig.SettingDbUsername)
3✔
126
        if username != "" {
3✔
127
                credentials := mopts.Credential{
×
128
                        Username: c.GetString(dconfig.SettingDbUsername),
×
129
                }
×
130
                password := c.GetString(dconfig.SettingDbPassword)
×
131
                if password != "" {
×
132
                        credentials.Password = password
×
133
                        credentials.PasswordSet = true
×
134
                }
×
135
                clientOptions.SetAuth(credentials)
×
136
        }
137

138
        if c.GetBool(dconfig.SettingDbSSL) {
3✔
139
                tlsConfig := &tls.Config{}
×
140
                tlsConfig.InsecureSkipVerify = c.GetBool(dconfig.SettingDbSSLSkipVerify)
×
141
                clientOptions.SetTLSConfig(tlsConfig)
×
142
        }
×
143

144
        // Set 10s timeout
145
        if _, ok := ctx.Deadline(); !ok {
6✔
146
                var cancel context.CancelFunc
3✔
147
                ctx, cancel = context.WithTimeout(ctx, ConnectTimeoutSeconds*time.Second)
3✔
148
                defer cancel()
3✔
149
        }
3✔
150
        client, err := mongo.Connect(ctx, clientOptions)
3✔
151
        if err != nil {
3✔
152
                return nil, errors.Wrap(err, "Failed to connect to mongo server")
×
153
        }
×
154

155
        // Validate connection
156
        if err = client.Ping(ctx, nil); err != nil {
3✔
157
                return nil, errors.Wrap(err, "Error reaching mongo server")
×
158
        }
×
159

160
        return client, nil
3✔
161
}
162

163
// DataStoreMongo is the data storage service
164
type DataStoreMongo struct {
165
        // client holds the reference to the client used to communicate with the
166
        // mongodb server.
167
        client *mongo.Client
168

169
        *Config
170
}
171

172
// NewDataStoreWithClient initializes a DataStore object
173
func NewDataStoreWithClient(client *mongo.Client, conf ...*Config) *DataStoreMongo {
41✔
174
        return &DataStoreMongo{
41✔
175
                client: client,
41✔
176
                Config: mergeConfig(conf...),
41✔
177
        }
41✔
178
}
41✔
179

180
// Ping verifies the connection to the database
181
func (db *DataStoreMongo) Ping(ctx context.Context) error {
×
182
        res := db.client.Database(*db.DbName).RunCommand(ctx, bson.M{"ping": 1})
×
183
        return res.Err()
×
184
}
×
185

186
func (db *DataStoreMongo) Close() error {
3✔
187
        ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
3✔
188
        defer cancel()
3✔
189
        err := db.client.Disconnect(ctx)
3✔
190
        return err
3✔
191
}
3✔
192

193
func (db *DataStoreMongo) Collection(
194
        name string,
195
        opts ...*mopts.CollectionOptions,
196
) *mongo.Collection {
84✔
197
        return db.client.Database(*db.DbName).Collection(name, opts...)
84✔
198
}
84✔
199

200
func (db *DataStoreMongo) ListCollectionNames(
201
        ctx context.Context,
202
) ([]string, error) {
1✔
203
        return db.client.Database(*db.DbName).ListCollectionNames(ctx, mopts.ListCollectionsOptions{})
1✔
204
}
1✔
205

206
func (db *DataStoreMongo) GetIntegrations(
207
        ctx context.Context,
208
        fltr model.IntegrationFilter,
209
) ([]model.Integration, error) {
20✔
210
        var (
20✔
211
                err      error
20✔
212
                tenantID string
20✔
213
                results  = []model.Integration{}
20✔
214
        )
20✔
215
        id := identity.FromContext(ctx)
20✔
216
        if id != nil {
39✔
217
                tenantID = id.Tenant
19✔
218
        }
19✔
219

220
        collIntegrations := db.Collection(CollNameIntegrations)
20✔
221
        findOpts := mopts.Find().
20✔
222
                SetSort(bson.D{{
20✔
223
                        Key:   KeyProvider,
20✔
224
                        Value: 1,
20✔
225
                }, {
20✔
226
                        Key:   KeyID,
20✔
227
                        Value: 1,
20✔
228
                }}).SetSkip(fltr.Skip)
20✔
229
        if fltr.Limit > 0 {
27✔
230
                findOpts.SetLimit(fltr.Limit)
7✔
231
        }
7✔
232

233
        fltrDoc := make(bson.D, 0, 3)
20✔
234
        fltrDoc = append(fltrDoc, bson.E{Key: KeyTenantID, Value: tenantID})
20✔
235
        if fltr.Provider != model.ProviderEmpty {
21✔
236
                fltrDoc = append(fltrDoc, bson.E{Key: KeyProvider, Value: fltr.Provider})
1✔
237
        }
1✔
238
        if fltr.IDs != nil {
23✔
239
                switch len(fltr.IDs) {
3✔
240
                case 0:
1✔
241
                        // Won't match anything, let's save the request
1✔
242
                        return results, nil
1✔
243
                case 1:
1✔
244
                        fltrDoc = append(fltrDoc, bson.E{Key: KeyID, Value: fltr.IDs[0]})
1✔
245

246
                default:
1✔
247
                        fltrDoc = append(fltrDoc, bson.E{Key: KeyID, Value: bson.D{{
1✔
248
                                Key: "$in", Value: fltr.IDs,
1✔
249
                        }}})
1✔
250
                }
251
        }
252

253
        cur, err := collIntegrations.Find(ctx,
19✔
254
                fltrDoc,
19✔
255
                findOpts,
19✔
256
        )
19✔
257
        if err != nil {
20✔
258
                return nil, errors.Wrap(err, "error executing integrations collection request")
1✔
259
        }
1✔
260
        if err = cur.All(ctx, &results); err != nil {
19✔
261
                return nil, errors.Wrap(err, "error retrieving integrations collection results")
1✔
262
        }
1✔
263

264
        return results, nil
17✔
265
}
266

267
func (db *DataStoreMongo) GetIntegrationById(
268
        ctx context.Context,
269
        integrationId uuid.UUID,
270
) (*model.Integration, error) {
3✔
271
        var integration = new(model.Integration)
3✔
272

3✔
273
        collIntegrations := db.Collection(CollNameIntegrations)
3✔
274
        tenantId := ""
3✔
275
        id := identity.FromContext(ctx)
3✔
276
        if id != nil {
5✔
277
                tenantId = id.Tenant
2✔
278
        }
2✔
279

280
        if err := collIntegrations.FindOne(ctx,
3✔
281
                bson.M{KeyTenantID: tenantId},
3✔
282
        ).Decode(&integration); err != nil {
5✔
283
                switch err {
2✔
284
                case mongo.ErrNoDocuments:
1✔
285
                        return nil, store.ErrObjectNotFound
1✔
286
                default:
1✔
287
                        return nil, errors.Wrap(err, ErrFailedToGetIntegrations.Error())
1✔
288
                }
289
        }
290
        return integration, nil
1✔
291
}
292

293
func (db *DataStoreMongo) CreateIntegration(
294
        ctx context.Context,
295
        integration model.Integration,
296
) (*model.Integration, error) {
10✔
297
        var tenantID string
10✔
298
        if id := identity.FromContext(ctx); id != nil {
18✔
299
                tenantID = id.Tenant
8✔
300
        }
8✔
301
        collIntegrations := db.Collection(CollNameIntegrations)
10✔
302

10✔
303
        // Force a single integration per tenant by utilizing unique '_id' index
10✔
304
        integration.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte(tenantID))
10✔
305

10✔
306
        _, err := collIntegrations.
10✔
307
                InsertOne(ctx, mstore.WithTenantID(ctx, integration))
10✔
308
        if err != nil {
11✔
309
                if isDuplicateKeyError(err) {
1✔
310
                        return nil, store.ErrObjectExists
×
311
                }
×
312
                return nil, errors.Wrapf(err, "failed to store integration %v", integration)
1✔
313
        }
314

315
        return &integration, err
9✔
316
}
317

318
func (db *DataStoreMongo) SetIntegrationCredentials(
319
        ctx context.Context,
320
        integrationId uuid.UUID,
321
        credentials model.Credentials,
322
) error {
2✔
323
        collIntegrations := db.client.Database(*db.DbName).Collection(CollNameIntegrations)
2✔
324

2✔
325
        fltr := bson.D{{
2✔
326
                Key:   KeyID,
2✔
327
                Value: integrationId,
2✔
328
        }}
2✔
329

2✔
330
        update := bson.M{
2✔
331
                "$set": bson.D{
2✔
332
                        {
2✔
333
                                Key:   KeyCredentials,
2✔
334
                                Value: credentials,
2✔
335
                        },
2✔
336
                },
2✔
337
        }
2✔
338

2✔
339
        result, err := collIntegrations.UpdateOne(ctx,
2✔
340
                mstore.WithTenantID(ctx, fltr),
2✔
341
                update,
2✔
342
        )
2✔
343
        if result.MatchedCount == 0 {
3✔
344
                return store.ErrObjectNotFound
1✔
345
        }
1✔
346

347
        return errors.Wrap(err, "mongo: failed to set integration credentials")
1✔
348
}
349

350
func (db *DataStoreMongo) RemoveIntegration(ctx context.Context, integrationId uuid.UUID) error {
2✔
351
        collIntegrations := db.client.Database(*db.DbName).Collection(CollNameIntegrations)
2✔
352
        fltr := bson.D{{
2✔
353
                Key:   KeyID,
2✔
354
                Value: integrationId,
2✔
355
        }}
2✔
356
        res, err := collIntegrations.DeleteOne(ctx, mstore.WithTenantID(ctx, fltr))
2✔
357
        if err != nil {
2✔
358
                return err
×
359
        } else if res.DeletedCount == 0 {
3✔
360
                return store.ErrObjectNotFound
1✔
361
        }
1✔
362
        return nil
1✔
363
}
364

365
// DoDevicesExistByIntegrationID checks if there is at least one device connected
366
// with given integration ID
367
func (db *DataStoreMongo) DoDevicesExistByIntegrationID(
368
        ctx context.Context,
369
        integrationID uuid.UUID,
370
) (bool, error) {
2✔
371
        var (
2✔
372
                err error
2✔
373
        )
2✔
374
        collDevices := db.client.Database(*db.DbName).Collection(CollNameDevices)
2✔
375

2✔
376
        fltr := bson.D{
2✔
377
                {
2✔
378
                        Key: KeyIntegrationIDs, Value: integrationID,
2✔
379
                },
2✔
380
        }
2✔
381
        if err = collDevices.FindOne(ctx, mstore.WithTenantID(ctx, fltr)).Err(); err != nil {
3✔
382
                if err == mongo.ErrNoDocuments {
2✔
383
                        return false, nil
1✔
384
                } else {
1✔
385
                        return false, err
×
386
                }
×
387
        }
388
        return true, nil
1✔
389
}
390

391
func (db *DataStoreMongo) GetDeviceByIntegrationID(
392
        ctx context.Context,
393
        deviceID string,
394
        integrationID uuid.UUID,
395
) (*model.Device, error) {
4✔
396
        var device *model.Device
4✔
397

4✔
398
        collDevices := db.Collection(CollNameDevices)
4✔
399
        tenantId := ""
4✔
400
        id := identity.FromContext(ctx)
4✔
401
        if id != nil {
6✔
402
                tenantId = id.Tenant
2✔
403
        }
2✔
404

405
        filter := bson.D{{
4✔
406
                Key: KeyTenantID, Value: tenantId,
4✔
407
        }, {
4✔
408
                Key: KeyID, Value: deviceID,
4✔
409
        }, {
4✔
410
                Key: KeyIntegrationIDs, Value: integrationID,
4✔
411
        }}
4✔
412
        if err := collDevices.FindOne(ctx,
4✔
413
                filter,
4✔
414
        ).Decode(&device); err != nil {
6✔
415
                switch err {
2✔
416
                case mongo.ErrNoDocuments:
1✔
417
                        return nil, store.ErrObjectNotFound
1✔
418
                default:
1✔
419
                        return nil, errors.Wrap(err, ErrFailedToGetDevice.Error())
1✔
420
                }
421
        }
422
        return device, nil
2✔
423
}
424

425
func (db *DataStoreMongo) GetDevice(
426
        ctx context.Context,
427
        deviceID string,
428
) (*model.Device, error) {
12✔
429
        var (
12✔
430
                tenantID string
12✔
431
                result   *model.Device = new(model.Device)
12✔
432
        )
12✔
433
        if id := identity.FromContext(ctx); id != nil {
21✔
434
                tenantID = id.Tenant
9✔
435
        }
9✔
436
        filter := bson.D{{
12✔
437
                Key: KeyID, Value: deviceID,
12✔
438
        }, {
12✔
439
                Key: KeyTenantID, Value: tenantID,
12✔
440
        }}
12✔
441
        collDevices := db.Collection(CollNameDevices)
12✔
442

12✔
443
        err := collDevices.FindOne(ctx, filter).
12✔
444
                Decode(result)
12✔
445
        if err == mongo.ErrNoDocuments {
16✔
446
                return nil, store.ErrObjectNotFound
4✔
447
        }
4✔
448
        return result, err
8✔
449
}
450

451
func (db *DataStoreMongo) DeleteDevice(ctx context.Context, deviceID string) error {
10✔
452
        var tenantID string
10✔
453
        if id := identity.FromContext(ctx); id != nil {
18✔
454
                tenantID = id.Tenant
8✔
455
        }
8✔
456
        collDevices := db.Collection(CollNameDevices)
10✔
457

10✔
458
        filter := bson.D{{
10✔
459
                Key: KeyID, Value: deviceID,
10✔
460
        }, {
10✔
461
                Key: KeyTenantID, Value: tenantID,
10✔
462
        }}
10✔
463

10✔
464
        res, err := collDevices.DeleteOne(ctx, filter)
10✔
465
        if err != nil {
11✔
466
                return err
1✔
467
        } else if res.DeletedCount == 0 {
13✔
468
                return store.ErrObjectNotFound
3✔
469
        }
3✔
470
        return nil
6✔
471
}
472

473
func (db *DataStoreMongo) RemoveDevicesFromIntegration(
474
        ctx context.Context,
475
        integrationID uuid.UUID,
476
) (int64, error) {
×
477
        var tenantID string
×
478
        if id := identity.FromContext(ctx); id != nil {
×
479
                tenantID = id.Tenant
×
480
        }
×
481
        filter := bson.D{{
×
482
                Key: KeyTenantID, Value: tenantID,
×
483
        }, {
×
484
                Key: KeyIntegrationIDs, Value: integrationID,
×
485
        }}
×
486
        update := bson.D{{
×
487
                Key: "$pull", Value: bson.D{{
×
488
                        Key: KeyIntegrationIDs, Value: integrationID,
×
489
                }},
×
490
        }}
×
491

×
492
        collDevices := db.Collection(CollNameDevices)
×
493

×
494
        res, err := collDevices.UpdateMany(ctx, filter, update)
×
495
        if res != nil {
×
496
                return res.ModifiedCount, err
×
497
        }
×
498
        return 0, errors.Wrap(err, "mongo: failed to remove device from integration")
×
499
}
500

501
func (db *DataStoreMongo) UpsertDeviceIntegrations(
502
        ctx context.Context,
503
        deviceID string,
504
        integrationIDs []uuid.UUID,
505
) (*model.Device, error) {
5✔
506
        var (
5✔
507
                tenantID string
5✔
508
                result   = new(model.Device)
5✔
509
        )
5✔
510
        if id := identity.FromContext(ctx); id != nil {
8✔
511
                tenantID = id.Tenant
3✔
512
        }
3✔
513
        if integrationIDs == nil {
7✔
514
                integrationIDs = []uuid.UUID{}
2✔
515
        }
2✔
516
        filter := bson.D{{
5✔
517
                Key: KeyID, Value: deviceID,
5✔
518
        }, {
5✔
519
                Key: KeyTenantID, Value: tenantID,
5✔
520
        }}
5✔
521
        update := bson.D{{
5✔
522
                Key: "$addToSet", Value: bson.D{{
5✔
523
                        Key: KeyIntegrationIDs, Value: bson.D{{
5✔
524
                                Key: "$each", Value: integrationIDs,
5✔
525
                        }},
5✔
526
                }},
5✔
527
        }}
5✔
528
        updateOpts := mopts.FindOneAndUpdate().
5✔
529
                SetUpsert(true).
5✔
530
                SetReturnDocument(mopts.After)
5✔
531
        collDevices := db.Collection(CollNameDevices)
5✔
532
        err := collDevices.FindOneAndUpdate(ctx, filter, update, updateOpts).
5✔
533
                Decode(result)
5✔
534
        return result, err
5✔
535
}
536

537
func (db *DataStoreMongo) GetAllDevices(ctx context.Context) (store.Iterator, error) {
2✔
538
        collDevs := db.Collection(CollNameDevices)
2✔
539

2✔
540
        return collDevs.Find(ctx,
2✔
541
                bson.D{},
2✔
542
                mopts.Find().
2✔
543
                        SetSort(bson.D{{Key: KeyTenantID, Value: 1}}),
2✔
544
        )
2✔
545

2✔
546
}
2✔
547

548
func (db *DataStoreMongo) DeleteTenantData(
549
        ctx context.Context,
550
) error {
3✔
551
        id := identity.FromContext(ctx)
3✔
552
        if id == nil {
4✔
553
                return errors.New("identity is empty")
1✔
554
        }
1✔
555
        if len(id.Tenant) < 1 {
3✔
556
                return errors.New("tenant id is empty")
1✔
557
        }
1✔
558

559
        collectionNames, err := db.ListCollectionNames(ctx)
1✔
560
        if err != nil {
1✔
561
                return err
×
562
        }
×
563
        for _, collName := range collectionNames {
4✔
564
                collection := db.Collection(collName)
3✔
565
                _, e := collection.DeleteMany(ctx, bson.M{KeyTenantID: id.Tenant})
3✔
566
                if e != nil {
3✔
567
                        return e
×
568
                }
×
569
        }
570
        return nil
1✔
571
}
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