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

mendersoftware / mender-server / 1495380963

14 Oct 2024 03:35PM UTC coverage: 70.373% (-2.5%) from 72.904%
1495380963

Pull #101

gitlab-ci

mineralsfree
feat: tenant list added

Ticket: MEN-7568
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #101: feat: tenant list added

4406 of 6391 branches covered (68.94%)

Branch coverage included in aggregate %.

88 of 183 new or added lines in 10 files covered. (48.09%)

2623 existing lines in 65 files now uncovered.

36673 of 51982 relevant lines covered (70.55%)

31.07 hits per line

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

73.01
/backend/services/iot-manager/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/mender-server/pkg/config"
31
        "github.com/mendersoftware/mender-server/pkg/identity"
32
        mstore "github.com/mendersoftware/mender-server/pkg/store/v2"
33

34
        dconfig "github.com/mendersoftware/mender-server/services/iot-manager/config"
35
        "github.com/mendersoftware/mender-server/services/iot-manager/model"
36
        "github.com/mendersoftware/mender-server/services/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 {
1✔
65
        conf := new(Config)
1✔
66
        return conf.
1✔
67
                SetAutomigrate(defaultAutomigrate).
1✔
68
                SetDbName(DbName)
1✔
69
}
1✔
70

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

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

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

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

×
UNCOV
107
        return dataStore, dataStore.Migrate(ctx)
×
108
}
109

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

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

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

×
UNCOV
125
        username := c.GetString(dconfig.SettingDbUsername)
×
UNCOV
126
        if username != "" {
×
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

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

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

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

UNCOV
160
        return client, nil
×
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 {
1✔
174
        return &DataStoreMongo{
1✔
175
                client: client,
1✔
176
                Config: mergeConfig(conf...),
1✔
177
        }
1✔
178
}
1✔
179

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

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

193
func (db *DataStoreMongo) Collection(
194
        name string,
195
        opts ...*mopts.CollectionOptions,
196
) *mongo.Collection {
1✔
197
        return db.client.Database(*db.DbName).Collection(name, opts...)
1✔
198
}
1✔
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) {
1✔
210
        var (
1✔
211
                err      error
1✔
212
                tenantID string
1✔
213
                results  = []model.Integration{}
1✔
214
        )
1✔
215
        id := identity.FromContext(ctx)
1✔
216
        if id != nil {
2✔
217
                tenantID = id.Tenant
1✔
218
        }
1✔
219

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

233
        fltrDoc := make(bson.D, 0, 3)
1✔
234
        fltrDoc = append(fltrDoc, bson.E{Key: KeyTenantID, Value: tenantID})
1✔
235
        if fltr.Provider != model.ProviderEmpty {
2✔
236
                fltrDoc = append(fltrDoc, bson.E{Key: KeyProvider, Value: fltr.Provider})
1✔
237
        }
1✔
238
        if fltr.IDs != nil {
2✔
239
                switch len(fltr.IDs) {
1✔
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,
1✔
254
                fltrDoc,
1✔
255
                findOpts,
1✔
256
        )
1✔
257
        if err != nil {
2✔
258
                return nil, errors.Wrap(err, "error executing integrations collection request")
1✔
259
        }
1✔
260
        if err = cur.All(ctx, &results); err != nil {
2✔
261
                return nil, errors.Wrap(err, "error retrieving integrations collection results")
1✔
262
        }
1✔
263

264
        return results, nil
1✔
265
}
266

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

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

280
        if err := collIntegrations.FindOne(ctx,
1✔
281
                bson.M{KeyTenantID: tenantId},
1✔
282
        ).Decode(&integration); err != nil {
2✔
283
                switch err {
1✔
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) {
1✔
297
        var tenantID string
1✔
298
        if id := identity.FromContext(ctx); id != nil {
2✔
299
                tenantID = id.Tenant
1✔
300
        }
1✔
301
        collIntegrations := db.Collection(CollNameIntegrations)
1✔
302

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

1✔
306
        _, err := collIntegrations.
1✔
307
                InsertOne(ctx, mstore.WithTenantID(ctx, integration))
1✔
308
        if err != nil {
2✔
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
1✔
316
}
317

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

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

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

1✔
339
        result, err := collIntegrations.UpdateOne(ctx,
1✔
340
                mstore.WithTenantID(ctx, fltr),
1✔
341
                update,
1✔
342
        )
1✔
343
        if result.MatchedCount == 0 {
2✔
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 {
1✔
351
        collIntegrations := db.client.Database(*db.DbName).Collection(CollNameIntegrations)
1✔
352
        fltr := bson.D{{
1✔
353
                Key:   KeyID,
1✔
354
                Value: integrationId,
1✔
355
        }}
1✔
356
        res, err := collIntegrations.DeleteOne(ctx, mstore.WithTenantID(ctx, fltr))
1✔
357
        if err != nil {
1✔
358
                return err
×
359
        } else if res.DeletedCount == 0 {
2✔
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) {
1✔
371
        var (
1✔
372
                err error
1✔
373
        )
1✔
374
        collDevices := db.client.Database(*db.DbName).Collection(CollNameDevices)
1✔
375

1✔
376
        fltr := bson.D{
1✔
377
                {
1✔
378
                        Key: KeyIntegrationIDs, Value: integrationID,
1✔
379
                },
1✔
380
        }
1✔
381
        if err = collDevices.FindOne(ctx, mstore.WithTenantID(ctx, fltr)).Err(); err != nil {
2✔
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) {
1✔
396
        var device *model.Device
1✔
397

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

405
        filter := bson.D{{
1✔
406
                Key: KeyTenantID, Value: tenantId,
1✔
407
        }, {
1✔
408
                Key: KeyID, Value: deviceID,
1✔
409
        }, {
1✔
410
                Key: KeyIntegrationIDs, Value: integrationID,
1✔
411
        }}
1✔
412
        if err := collDevices.FindOne(ctx,
1✔
413
                filter,
1✔
414
        ).Decode(&device); err != nil {
2✔
415
                switch err {
1✔
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
1✔
423
}
424

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

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

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

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

1✔
464
        res, err := collDevices.DeleteOne(ctx, filter)
1✔
465
        if err != nil {
2✔
466
                return err
1✔
467
        } else if res.DeletedCount == 0 {
3✔
468
                return store.ErrObjectNotFound
1✔
469
        }
1✔
470
        return nil
1✔
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) {
1✔
506
        var (
1✔
507
                tenantID string
1✔
508
                result   = new(model.Device)
1✔
509
        )
1✔
510
        if id := identity.FromContext(ctx); id != nil {
2✔
511
                tenantID = id.Tenant
1✔
512
        }
1✔
513
        if integrationIDs == nil {
2✔
514
                integrationIDs = []uuid.UUID{}
1✔
515
        }
1✔
516
        filter := bson.D{{
1✔
517
                Key: KeyID, Value: deviceID,
1✔
518
        }, {
1✔
519
                Key: KeyTenantID, Value: tenantID,
1✔
520
        }}
1✔
521
        update := bson.D{{
1✔
522
                Key: "$addToSet", Value: bson.D{{
1✔
523
                        Key: KeyIntegrationIDs, Value: bson.D{{
1✔
524
                                Key: "$each", Value: integrationIDs,
1✔
525
                        }},
1✔
526
                }},
1✔
527
        }}
1✔
528
        updateOpts := mopts.FindOneAndUpdate().
1✔
529
                SetUpsert(true).
1✔
530
                SetReturnDocument(mopts.After)
1✔
531
        collDevices := db.Collection(CollNameDevices)
1✔
532
        err := collDevices.FindOneAndUpdate(ctx, filter, update, updateOpts).
1✔
533
                Decode(result)
1✔
534
        return result, err
1✔
535
}
536

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

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

×
UNCOV
546
}
×
547

548
func (db *DataStoreMongo) DeleteTenantData(
549
        ctx context.Context,
550
) error {
1✔
551
        id := identity.FromContext(ctx)
1✔
552
        if id == nil {
2✔
553
                return errors.New("identity is empty")
1✔
554
        }
1✔
555
        if len(id.Tenant) < 1 {
2✔
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 {
2✔
564
                collection := db.Collection(collName)
1✔
565
                _, e := collection.DeleteMany(ctx, bson.M{KeyTenantID: id.Tenant})
1✔
566
                if e != nil {
1✔
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