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

mendersoftware / deployments / 929748838

pending completion
929748838

Pull #884

gitlab-ci

merlin-northern
test: db units for update release.

Ticket: MEN-6593
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #884: feat: release notes: update and store.

268 of 340 new or added lines in 6 files covered. (78.82%)

415 existing lines in 5 files now uncovered.

7650 of 9637 relevant lines covered (79.38%)

33.69 hits per line

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

77.13
/app/app.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 app
16

17
import (
18
        "bytes"
19
        "context"
20
        "encoding/json"
21
        "io"
22
        "path"
23
        "reflect"
24
        "strings"
25
        "time"
26

27
        "github.com/google/uuid"
28
        "github.com/pkg/errors"
29

30
        "github.com/mendersoftware/go-lib-micro/identity"
31
        "github.com/mendersoftware/go-lib-micro/log"
32
        "github.com/mendersoftware/mender-artifact/areader"
33
        "github.com/mendersoftware/mender-artifact/artifact"
34
        "github.com/mendersoftware/mender-artifact/awriter"
35
        "github.com/mendersoftware/mender-artifact/handlers"
36

37
        "github.com/mendersoftware/deployments/client/inventory"
38
        "github.com/mendersoftware/deployments/client/reporting"
39
        "github.com/mendersoftware/deployments/client/workflows"
40
        "github.com/mendersoftware/deployments/model"
41
        "github.com/mendersoftware/deployments/storage"
42
        "github.com/mendersoftware/deployments/store"
43
        "github.com/mendersoftware/deployments/store/mongo"
44
        "github.com/mendersoftware/deployments/utils"
45
)
46

47
const (
48
        ArtifactContentType              = "application/vnd.mender-artifact"
49
        ArtifactConfigureProvides        = "data-partition.mender-configure.version"
50
        ArtifactConfigureProvidesCleared = "data-partition.mender-configure.*"
51

52
        DefaultUpdateDownloadLinkExpire  = 24 * time.Hour
53
        DefaultImageGenerationLinkExpire = 7 * 24 * time.Hour
54
        PerPageInventoryDevices          = 512
55
        InventoryGroupScope              = "system"
56
        InventoryIdentityScope           = "identity"
57
        InventoryGroupAttributeName      = "group"
58
        InventoryStatusAttributeName     = "status"
59
        InventoryStatusAccepted          = "accepted"
60

61
        fileSuffixTmp = ".tmp"
62

63
        inprogressIdleTime = time.Hour
64
)
65

66
var (
67
        ArtifactConfigureType = "mender-configure"
68
)
69

70
// Errors expected from App interface
71
var (
72
        // images
73
        ErrImageMetaNotFound                = errors.New("Image metadata is not found")
74
        ErrModelMultipartUploadMsgMalformed = errors.New("Multipart upload message malformed")
75
        ErrModelMissingInputMetadata        = errors.New("Missing input metadata")
76
        ErrModelMissingInputArtifact        = errors.New("Missing input artifact")
77
        ErrModelInvalidMetadata             = errors.New("Metadata invalid")
78
        ErrModelArtifactNotUnique           = errors.New("Artifact not unique")
79
        ErrModelImageInActiveDeployment     = errors.New(
80
                "Image is used in active deployment and cannot be removed",
81
        )
82
        ErrModelImageUsedInAnyDeployment = errors.New("Image has already been used in deployment")
83
        ErrModelParsingArtifactFailed    = errors.New("Cannot parse artifact file")
84
        ErrUploadNotFound                = errors.New("artifact object not found")
85
        ErrEmptyArtifact                 = errors.New("artifact cannot be nil")
86

87
        ErrMsgArtifactConflict = "An artifact with the same name has conflicting dependencies"
88

89
        // deployments
90
        ErrModelMissingInput       = errors.New("Missing input deployment data")
91
        ErrModelInvalidDeviceID    = errors.New("Invalid device ID")
92
        ErrModelDeploymentNotFound = errors.New("Deployment not found")
93
        ErrModelInternal           = errors.New("Internal error")
94
        ErrStorageInvalidLog       = errors.New("Invalid deployment log")
95
        ErrStorageNotFound         = errors.New("Not found")
96
        ErrDeploymentAborted       = errors.New("Deployment aborted")
97
        ErrDeviceDecommissioned    = errors.New("Device decommissioned")
98
        ErrNoArtifact              = errors.New("No artifact for the deployment")
99
        ErrNoDevices               = errors.New("No devices for the deployment")
100
        ErrDuplicateDeployment     = errors.New("Deployment with given ID already exists")
101
        ErrInvalidDeploymentID     = errors.New("Deployment ID must be a valid UUID")
102
        ErrConflictingRequestData  = errors.New("Device provided conflicting request data")
103
)
104

105
//deployments
106

107
//go:generate ../utils/mockgen.sh
108
type App interface {
109
        HealthCheck(ctx context.Context) error
110
        // limits
111
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
112
        ProvisionTenant(ctx context.Context, tenant_id string) error
113

114
        // Storage Settings
115
        GetStorageSettings(ctx context.Context) (*model.StorageSettings, error)
116
        SetStorageSettings(ctx context.Context, storageSettings *model.StorageSettings) error
117

118
        // images
119
        ListImages(
120
                ctx context.Context,
121
                filters *model.ReleaseOrImageFilter,
122
        ) ([]*model.Image, int, error)
123
        DownloadLink(ctx context.Context, imageID string,
124
                expire time.Duration) (*model.Link, error)
125
        UploadLink(
126
                ctx context.Context,
127
                expire time.Duration,
128
                skipVerify bool,
129
        ) (*model.UploadLink, error)
130
        CompleteUpload(ctx context.Context, intentID string, skipVerify bool) error
131
        GetImage(ctx context.Context, id string) (*model.Image, error)
132
        DeleteImage(ctx context.Context, imageID string) error
133
        CreateImage(ctx context.Context,
134
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
135
        GenerateImage(ctx context.Context,
136
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
137
        GenerateConfigurationImage(
138
                ctx context.Context,
139
                deviceType string,
140
                deploymentID string,
141
        ) (io.Reader, error)
142
        EditImage(ctx context.Context, id string,
143
                constructorData *model.ImageMeta) (bool, error)
144

145
        // deployments
146
        CreateDeployment(ctx context.Context,
147
                constructor *model.DeploymentConstructor) (string, error)
148
        GetDeployment(ctx context.Context, deploymentID string) (*model.Deployment, error)
149
        IsDeploymentFinished(ctx context.Context, deploymentID string) (bool, error)
150
        AbortDeployment(ctx context.Context, deploymentID string) error
151
        GetDeploymentStats(ctx context.Context, deploymentID string) (model.Stats, error)
152
        GetDeploymentsStats(ctx context.Context,
153
                deploymentIDs ...string) ([]*model.DeploymentStats, error)
154
        GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
155
                request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error)
156
        HasDeploymentForDevice(ctx context.Context, deploymentID string,
157
                deviceID string) (bool, error)
158
        UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
159
                deviceID string, state model.DeviceDeploymentState) error
160
        GetDeviceStatusesForDeployment(ctx context.Context,
161
                deploymentID string) ([]model.DeviceDeployment, error)
162
        GetDevicesListForDeployment(ctx context.Context,
163
                query store.ListQuery) ([]model.DeviceDeployment, int, error)
164
        GetDeviceDeploymentListForDevice(ctx context.Context,
165
                query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error)
166
        LookupDeployment(ctx context.Context,
167
                query model.Query) ([]*model.Deployment, int64, error)
168
        SaveDeviceDeploymentLog(ctx context.Context, deviceID string,
169
                deploymentID string, logs []model.LogMessage) error
170
        GetDeviceDeploymentLog(ctx context.Context,
171
                deviceID, deploymentID string) (*model.DeploymentLog, error)
172
        AbortDeviceDeployments(ctx context.Context, deviceID string) error
173
        DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error
174
        DecommissionDevice(ctx context.Context, deviceID string) error
175
        CreateDeviceConfigurationDeployment(
176
                ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
177
                deviceID, deploymentID string) (string, error)
178
        UpdateDeploymentsWithArtifactName(
179
                ctx context.Context,
180
                artifactName string,
181
        ) error
182
        GetDeviceDeploymentLastStatus(
183
                ctx context.Context,
184
                devicesIds []string,
185
        ) (
186
                model.DeviceDeploymentLastStatuses,
187
                error,
188
        )
189

190
        // releases
191
        ReplaceReleaseTags(ctx context.Context, releaseName string, tags model.Tags) error
192
        UpdateRelease(ctx context.Context, releaseName string, release model.ReleasePatch) error
193
        ListReleaseTags(ctx context.Context) (model.Tags, error)
194
}
195

196
type Deployments struct {
197
        db              store.DataStore
198
        objectStorage   storage.ObjectStorage
199
        workflowsClient workflows.Client
200
        inventoryClient inventory.Client
201
        reportingClient reporting.Client
202
}
203

204
// Compile-time check
205
var _ App = &Deployments{}
206

207
func NewDeployments(
208
        storage store.DataStore,
209
        objectStorage storage.ObjectStorage,
210
) *Deployments {
72✔
211
        return &Deployments{
72✔
212
                db:              storage,
72✔
213
                objectStorage:   objectStorage,
72✔
214
                workflowsClient: workflows.NewClient(),
72✔
215
                inventoryClient: inventory.NewClient(),
72✔
216
        }
72✔
217
}
72✔
218

219
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
4✔
220
        d.workflowsClient = workflowsClient
4✔
221
}
4✔
222

223
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
8✔
224
        d.inventoryClient = inventoryClient
8✔
225
}
8✔
226

227
func (d *Deployments) HealthCheck(ctx context.Context) error {
6✔
228
        err := d.db.Ping(ctx)
6✔
229
        if err != nil {
7✔
230
                return errors.Wrap(err, "error reaching MongoDB")
1✔
231
        }
1✔
232
        err = d.objectStorage.HealthCheck(ctx)
5✔
233
        if err != nil {
6✔
234
                return errors.Wrap(
1✔
235
                        err,
1✔
236
                        "error reaching artifact storage service",
1✔
237
                )
1✔
238
        }
1✔
239

240
        err = d.workflowsClient.CheckHealth(ctx)
4✔
241
        if err != nil {
5✔
242
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
243
        }
1✔
244

245
        err = d.inventoryClient.CheckHealth(ctx)
3✔
246
        if err != nil {
4✔
247
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
248
        }
1✔
249

250
        if d.reportingClient != nil {
4✔
251
                err = d.reportingClient.CheckHealth(ctx)
2✔
252
                if err != nil {
3✔
253
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
254
                }
1✔
255
        }
256
        return nil
1✔
257
}
258

259
func (d *Deployments) contextWithStorageSettings(
260
        ctx context.Context,
261
) (context.Context, error) {
26✔
262
        var err error
26✔
263
        settings, ok := storage.SettingsFromContext(ctx)
26✔
264
        if !ok {
48✔
265
                settings, err = d.db.GetStorageSettings(ctx)
22✔
266
        }
22✔
267
        if err != nil {
28✔
268
                return nil, err
2✔
269
        } else if settings != nil {
26✔
270
                err = settings.Validate()
×
271
                if err != nil {
×
272
                        return nil, err
×
273
                }
×
274
        }
275
        return storage.SettingsWithContext(ctx, settings), nil
24✔
276
}
277

278
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
3✔
279
        limit, err := d.db.GetLimit(ctx, name)
3✔
280
        if err == mongo.ErrLimitNotFound {
4✔
281
                return &model.Limit{
1✔
282
                        Name:  name,
1✔
283
                        Value: 0,
1✔
284
                }, nil
1✔
285

1✔
286
        } else if err != nil {
4✔
287
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
288
        }
1✔
289
        return limit, nil
1✔
290
}
291

292
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
3✔
293
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
4✔
294
                return errors.Wrap(err, "failed to provision tenant")
1✔
295
        }
1✔
296

297
        return nil
2✔
298
}
299

300
// CreateImage parses artifact and uploads artifact file to the file storage - in parallel,
301
// and creates image structure in the system.
302
// Returns image ID and nil on success.
303
func (d *Deployments) CreateImage(ctx context.Context,
304
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
1✔
305
        return d.handleArtifact(ctx, multipartUploadMsg, false)
1✔
306
}
1✔
307

308
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
309
// and creates image structure in the system.
310
// Returns image ID, artifact file ID and nil on success.
311
func (d *Deployments) handleArtifact(ctx context.Context,
312
        multipartUploadMsg *model.MultipartUploadMsg,
313
        skipVerify bool,
314
) (string, error) {
5✔
315

5✔
316
        l := log.FromContext(ctx)
5✔
317
        ctx, err := d.contextWithStorageSettings(ctx)
5✔
318
        if err != nil {
5✔
319
                return "", err
×
320
        }
×
321

322
        // create pipe
323
        pR, pW := io.Pipe()
5✔
324

5✔
325
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
5✔
326

5✔
327
        tee := io.TeeReader(artifactReader, pW)
5✔
328

5✔
329
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
5✔
330
        if err != nil {
6✔
331
                uid, _ = uuid.NewRandom()
1✔
332
        }
1✔
333
        artifactID := uid.String()
5✔
334

5✔
335
        ch := make(chan error)
5✔
336
        // create goroutine for artifact upload
5✔
337
        //
5✔
338
        // reading from the pipe (which is done in UploadArtifact method) is a blocking operation
5✔
339
        // and cannot be done in the same goroutine as writing to the pipe
5✔
340
        //
5✔
341
        // uploading and parsing artifact in the same process will cause in a deadlock!
5✔
342
        //nolint:errcheck
5✔
343
        go func() (err error) {
10✔
344
                defer func() { ch <- err }()
10✔
345
                if skipVerify {
8✔
346
                        err = nil
3✔
347
                        io.Copy(io.Discard, pR)
3✔
348
                        return nil
3✔
349
                }
3✔
350
                err = d.objectStorage.PutObject(
3✔
351
                        ctx, model.ImagePathFromContext(ctx, artifactID), pR,
3✔
352
                )
3✔
353
                if err != nil {
4✔
354
                        pR.CloseWithError(err)
1✔
355
                }
1✔
356
                return err
3✔
357
        }()
358

359
        // parse artifact
360
        // artifact library reads all the data from the given reader
361
        metaArtifactConstructor, err := getMetaFromArchive(&tee, skipVerify)
5✔
362
        if err != nil {
10✔
363
                _ = pW.CloseWithError(err)
5✔
364
                <-ch
5✔
365
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
5✔
366
        }
5✔
367
        // validate artifact metadata
368
        if err = metaArtifactConstructor.Validate(); err != nil {
1✔
369
                return artifactID, ErrModelInvalidMetadata
×
370
        }
×
371

372
        if !skipVerify {
2✔
373
                // read the rest of the data,
1✔
374
                // just in case the artifact library did not read all the data from the reader
1✔
375
                _, err = io.Copy(io.Discard, tee)
1✔
376
                if err != nil {
1✔
377
                        // CloseWithError will cause the reading end to abort upload.
×
378
                        _ = pW.CloseWithError(err)
×
379
                        <-ch
×
380
                        return artifactID, err
×
381
                }
×
382
        }
383

384
        // close the pipe
385
        pW.Close()
1✔
386

1✔
387
        // collect output from the goroutine
1✔
388
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
389
                return artifactID, uploadResponseErr
×
390
        }
×
391

392
        image := model.NewImage(
1✔
393
                artifactID,
1✔
394
                multipartUploadMsg.MetaConstructor,
1✔
395
                metaArtifactConstructor,
1✔
396
                artifactReader.Count(),
1✔
397
        )
1✔
398

1✔
399
        // save image structure in the system
1✔
400
        if err = d.db.InsertImage(ctx, image); err != nil {
1✔
401
                // Try to remove the storage from s3.
×
402
                if errDelete := d.objectStorage.DeleteObject(
×
403
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
404
                ); errDelete != nil {
×
405
                        l.Errorf(
×
406
                                "failed to clean up artifact storage after failure: %s",
×
407
                                errDelete,
×
408
                        )
×
409
                }
×
410
                if idxErr, ok := err.(*model.ConflictError); ok {
×
411
                        return artifactID, idxErr
×
412
                }
×
413
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
414
        }
415

416
        // update release
417
        if err := d.updateRelease(ctx, image, nil); err != nil {
1✔
418
                return "", err
×
419
        }
×
420

421
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
422
                return "", errors.Wrap(err, "fail to update deployments")
×
423
        }
×
424

425
        return artifactID, nil
1✔
426
}
427

428
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
429
// creates image structure in the system, and starts the workflow to generate the
430
// artifact from them.
431
// Returns image ID and nil on success.
432
func (d *Deployments) GenerateImage(ctx context.Context,
433
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
11✔
434

11✔
435
        if multipartGenerateImageMsg == nil {
12✔
436
                return "", ErrModelMultipartUploadMsgMalformed
1✔
437
        }
1✔
438

439
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
10✔
440
        if err != nil {
15✔
441
                return "", err
5✔
442
        }
5✔
443
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
6✔
444
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
445
        }
1✔
446
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
5✔
447
        if err != nil {
7✔
448
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
3✔
449
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
450
                }
1✔
451
                return "", err
1✔
452
        }
453

454
        return multipartGenerateImageMsg.ArtifactID, err
3✔
455
}
456

457
func (d *Deployments) GenerateConfigurationImage(
458
        ctx context.Context,
459
        deviceType string,
460
        deploymentID string,
461
) (io.Reader, error) {
5✔
462
        var buf bytes.Buffer
5✔
463
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
464
        if err != nil {
6✔
465
                return nil, err
1✔
466
        } else if dpl == nil {
6✔
467
                return nil, ErrModelDeploymentNotFound
1✔
468
        }
1✔
469
        var metaData map[string]interface{}
3✔
470
        err = json.Unmarshal(dpl.Configuration, &metaData)
3✔
471
        if err != nil {
4✔
472
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
1✔
473
        }
1✔
474

475
        artieWriter := awriter.NewWriter(&buf, artifact.NewCompressorNone())
2✔
476
        module := handlers.NewModuleImage(ArtifactConfigureType)
2✔
477
        err = artieWriter.WriteArtifact(&awriter.WriteArtifactArgs{
2✔
478
                Format:  "mender",
2✔
479
                Version: 3,
2✔
480
                Devices: []string{deviceType},
2✔
481
                Name:    dpl.ArtifactName,
2✔
482
                Updates: &awriter.Updates{Updates: []handlers.Composer{module}},
2✔
483
                Depends: &artifact.ArtifactDepends{
2✔
484
                        CompatibleDevices: []string{deviceType},
2✔
485
                },
2✔
486
                Provides: &artifact.ArtifactProvides{
2✔
487
                        ArtifactName: dpl.ArtifactName,
2✔
488
                },
2✔
489
                MetaData: metaData,
2✔
490
                TypeInfoV3: &artifact.TypeInfoV3{
2✔
491
                        Type: &ArtifactConfigureType,
2✔
492
                        ArtifactProvides: artifact.TypeInfoProvides{
2✔
493
                                ArtifactConfigureProvides: dpl.ArtifactName,
2✔
494
                        },
2✔
495
                        ArtifactDepends:        artifact.TypeInfoDepends{},
2✔
496
                        ClearsArtifactProvides: []string{ArtifactConfigureProvidesCleared},
2✔
497
                },
2✔
498
        })
2✔
499

2✔
500
        return &buf, err
2✔
501
}
502

503
// handleRawFile parses raw data, uploads it to the file storage,
504
// and starts the workflow to generate the artifact.
505
// Returns the object path to the file and nil on success.
506
func (d *Deployments) handleRawFile(ctx context.Context,
507
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
10✔
508
        l := log.FromContext(ctx)
10✔
509
        uid, _ := uuid.NewRandom()
10✔
510
        artifactID := uid.String()
10✔
511
        multipartMsg.ArtifactID = artifactID
10✔
512
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
10✔
513

10✔
514
        // check if artifact is unique
10✔
515
        // artifact is considered to be unique if there is no artifact with the same name
10✔
516
        // and supporting the same platform in the system
10✔
517
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
10✔
518
                multipartMsg.Name,
10✔
519
                multipartMsg.DeviceTypesCompatible,
10✔
520
        )
10✔
521
        if err != nil {
11✔
522
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
1✔
523
        }
1✔
524
        if !isArtifactUnique {
10✔
525
                return "", ErrModelArtifactNotUnique
1✔
526
        }
1✔
527

528
        ctx, err = d.contextWithStorageSettings(ctx)
8✔
529
        if err != nil {
8✔
530
                return "", err
×
531
        }
×
532
        err = d.objectStorage.PutObject(
8✔
533
                ctx, filePath, multipartMsg.FileReader,
8✔
534
        )
8✔
535
        if err != nil {
9✔
536
                return "", err
1✔
537
        }
1✔
538
        defer func() {
14✔
539
                if err != nil {
9✔
540
                        e := d.objectStorage.DeleteObject(ctx, filePath)
2✔
541
                        if e != nil {
4✔
542
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
2✔
543
                                        filePath, e)
2✔
544
                        }
2✔
545
                }
546
        }()
547

548
        link, err := d.objectStorage.GetRequest(
7✔
549
                ctx,
7✔
550
                filePath,
7✔
551
                path.Base(filePath),
7✔
552
                DefaultImageGenerationLinkExpire,
7✔
553
        )
7✔
554
        if err != nil {
8✔
555
                return "", err
1✔
556
        }
1✔
557
        multipartMsg.GetArtifactURI = link.Uri
6✔
558

6✔
559
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
6✔
560
        if err != nil {
7✔
561
                return "", err
1✔
562
        }
1✔
563
        multipartMsg.DeleteArtifactURI = link.Uri
5✔
564

5✔
565
        return artifactID, nil
5✔
566
}
567

568
// GetImage allows to fetch image object with specified id
569
// Nil if not found
570
func (d *Deployments) GetImage(ctx context.Context, id string) (*model.Image, error) {
1✔
571

1✔
572
        image, err := d.db.FindImageByID(ctx, id)
1✔
573
        if err != nil {
1✔
574
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
575
        }
×
576

577
        if image == nil {
2✔
578
                return nil, nil
1✔
579
        }
1✔
580

581
        return image, nil
1✔
582
}
583

584
// DeleteImage removes metadata and image file
585
// Noop for not existing images
586
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
587
// file is needed
588
// In case of already finished updates only image file is not needed, metadata is attached directly
589
// to device deployment therefore we still have some information about image that have been used
590
// (but not the file)
591
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
1✔
592
        found, err := d.GetImage(ctx, imageID)
1✔
593

1✔
594
        if err != nil {
1✔
595
                return errors.Wrap(err, "Getting image metadata")
×
596
        }
×
597

598
        if found == nil {
1✔
599
                return ErrImageMetaNotFound
×
600
        }
×
601

602
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
603
        if err != nil {
1✔
604
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
605
        }
×
606

607
        // Image is in use, not allowed to delete
608
        if inUse {
2✔
609
                return ErrModelImageInActiveDeployment
1✔
610
        }
1✔
611

612
        // Delete image file (call to external service)
613
        // Noop for not existing file
614
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
615
        if err != nil {
1✔
616
                return err
×
617
        }
×
618
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
619
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
620
                return errors.Wrap(err, "Deleting image file")
×
621
        }
×
622

623
        // Delete metadata
624
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
625
                return errors.Wrap(err, "Deleting image metadata")
×
626
        }
×
627

628
        // update release
629
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
630
                return err
×
631
        }
×
632

633
        return nil
1✔
634
}
635

636
// ListImages according to specified filers.
637
func (d *Deployments) ListImages(
638
        ctx context.Context,
639
        filters *model.ReleaseOrImageFilter,
640
) ([]*model.Image, int, error) {
1✔
641
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
642
        if err != nil {
1✔
643
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
644
        }
×
645

646
        if imageList == nil {
2✔
647
                return make([]*model.Image, 0), 0, nil
1✔
648
        }
1✔
649

650
        return imageList, count, nil
1✔
651
}
652

653
// EditObject allows editing only if image have not been used yet in any deployment.
654
func (d *Deployments) EditImage(ctx context.Context, imageID string,
655
        constructor *model.ImageMeta) (bool, error) {
×
656

×
657
        if err := constructor.Validate(); err != nil {
×
658
                return false, errors.Wrap(err, "Validating image metadata")
×
659
        }
×
660

661
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
662
        if err != nil {
×
663
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
664
        }
×
665

666
        if found {
×
667
                return false, ErrModelImageUsedInAnyDeployment
×
668
        }
×
669

670
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
671
        if err != nil {
×
672
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
673
        }
×
674

675
        if foundImage == nil {
×
676
                return false, nil
×
677
        }
×
678

679
        foundImage.SetModified(time.Now())
×
680
        foundImage.ImageMeta = constructor
×
681

×
682
        _, err = d.db.Update(ctx, foundImage)
×
683
        if err != nil {
×
684
                return false, errors.Wrap(err, "Updating image matadata")
×
685
        }
×
686

687
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
688
                return false, err
×
689
        }
×
690

691
        return true, nil
×
692
}
693

694
// DownloadLink presigned GET link to download image file.
695
// Returns error if image have not been uploaded.
696
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
697
        expire time.Duration) (*model.Link, error) {
1✔
698

1✔
699
        image, err := d.GetImage(ctx, imageID)
1✔
700
        if err != nil {
1✔
701
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
702
        }
×
703

704
        if image == nil {
1✔
705
                return nil, nil
×
706
        }
×
707

708
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
709
        if err != nil {
1✔
710
                return nil, err
×
711
        }
×
712
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
713
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
714
        if err != nil {
1✔
715
                return nil, errors.Wrap(err, "Searching for image file")
×
716
        }
×
717

718
        link, err := d.objectStorage.GetRequest(
1✔
719
                ctx,
1✔
720
                imagePath,
1✔
721
                image.Name+model.ArtifactFileSuffix,
1✔
722
                expire,
1✔
723
        )
1✔
724
        if err != nil {
1✔
725
                return nil, errors.Wrap(err, "Generating download link")
×
726
        }
×
727

728
        return link, nil
1✔
729
}
730

731
func (d *Deployments) UploadLink(
732
        ctx context.Context,
733
        expire time.Duration,
734
        skipVerify bool,
735
) (*model.UploadLink, error) {
6✔
736
        ctx, err := d.contextWithStorageSettings(ctx)
6✔
737
        if err != nil {
7✔
738
                return nil, err
1✔
739
        }
1✔
740

741
        artifactID := uuid.New().String()
5✔
742
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
5✔
743
        if skipVerify {
6✔
744
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
745
        }
1✔
746
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
5✔
747
        if err != nil {
6✔
748
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
1✔
749
        }
1✔
750
        upLink := &model.UploadLink{
4✔
751
                ArtifactID: artifactID,
4✔
752
                IssuedAt:   time.Now(),
4✔
753
                Link:       *link,
4✔
754
        }
4✔
755
        err = d.db.InsertUploadIntent(ctx, upLink)
4✔
756
        if err != nil {
5✔
757
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
1✔
758
        }
1✔
759

760
        return upLink, err
3✔
761
}
762

763
func (d *Deployments) processUploadedArtifact(
764
        ctx context.Context,
765
        artifactID string,
766
        artifact io.ReadCloser,
767
        skipVerify bool,
768
) error {
5✔
769
        linkStatus := model.LinkStatusCompleted
5✔
770

5✔
771
        l := log.FromContext(ctx)
5✔
772
        defer artifact.Close()
5✔
773
        ctx, cancel := context.WithCancel(ctx)
5✔
774
        defer cancel()
5✔
775
        go func() { // Heatbeat routine
10✔
776
                ticker := time.NewTicker(inprogressIdleTime / 2)
5✔
777
                done := ctx.Done()
5✔
778
                defer ticker.Stop()
5✔
779
                for {
10✔
780
                        select {
5✔
781
                        case <-ticker.C:
×
782
                                err := d.db.UpdateUploadIntentStatus(
×
783
                                        ctx,
×
784
                                        artifactID,
×
785
                                        model.LinkStatusProcessing,
×
786
                                        model.LinkStatusProcessing,
×
787
                                )
×
788
                                if err != nil {
×
789
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
790
                                        cancel()
×
791
                                        return
×
792
                                }
×
793
                        case <-done:
5✔
794
                                return
5✔
795
                        }
796
                }
797
        }()
798
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
5✔
799
                ArtifactID:     artifactID,
5✔
800
                ArtifactReader: artifact,
5✔
801
        },
5✔
802
                skipVerify,
5✔
803
        )
5✔
804
        if err != nil {
9✔
805
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
4✔
806
                linkStatus = model.LinkStatusAborted
4✔
807
        }
4✔
808
        errDB := d.db.UpdateUploadIntentStatus(
5✔
809
                ctx, artifactID,
5✔
810
                model.LinkStatusProcessing, linkStatus,
5✔
811
        )
5✔
812
        if errDB != nil {
8✔
813
                l.Warnf("failed to update upload link status: %s", errDB)
3✔
814
        }
3✔
815
        return err
5✔
816
}
817

818
func (d *Deployments) CompleteUpload(
819
        ctx context.Context,
820
        intentID string,
821
        skipVerify bool,
822
) error {
10✔
823
        l := log.FromContext(ctx)
10✔
824
        idty := identity.FromContext(ctx)
10✔
825
        ctx, err := d.contextWithStorageSettings(ctx)
10✔
826
        if err != nil {
11✔
827
                return err
1✔
828
        }
1✔
829
        // Create an async context that doesn't cancel when server connection
830
        // closes.
831
        ctxAsync := context.Background()
9✔
832
        ctxAsync = log.WithContext(ctxAsync, l)
9✔
833
        ctxAsync = identity.WithContext(ctxAsync, idty)
9✔
834

9✔
835
        settings, _ := storage.SettingsFromContext(ctx)
9✔
836
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
9✔
837
        var artifactReader io.ReadCloser
9✔
838
        if skipVerify {
12✔
839
                artifactReader, err = d.objectStorage.GetObject(
3✔
840
                        ctxAsync,
3✔
841
                        model.ImagePathFromContext(ctx, intentID),
3✔
842
                )
3✔
843
        } else {
9✔
844
                artifactReader, err = d.objectStorage.GetObject(
6✔
845
                        ctxAsync,
6✔
846
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
6✔
847
                )
6✔
848
        }
6✔
849
        if err != nil {
11✔
850
                if errors.Is(err, storage.ErrObjectNotFound) {
3✔
851
                        return ErrUploadNotFound
1✔
852
                }
1✔
853
                return err
1✔
854
        }
855

856
        err = d.db.UpdateUploadIntentStatus(
7✔
857
                ctx,
7✔
858
                intentID,
7✔
859
                model.LinkStatusPending,
7✔
860
                model.LinkStatusProcessing,
7✔
861
        )
7✔
862
        if err != nil {
9✔
863
                errClose := artifactReader.Close()
2✔
864
                if errClose != nil {
3✔
865
                        l.Warnf("failed to close artifact reader: %s", errClose)
1✔
866
                }
1✔
867
                if errors.Is(err, store.ErrNotFound) {
3✔
868
                        return ErrUploadNotFound
1✔
869
                }
1✔
870
                return err
1✔
871
        }
872
        go d.processUploadedArtifact( // nolint:errcheck
5✔
873
                ctxAsync, intentID, artifactReader, skipVerify,
5✔
874
        )
5✔
875
        return nil
5✔
876
}
877

878
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
879
        return &model.ArtifactInfo{
1✔
880
                Format:  info.Format,
1✔
881
                Version: uint(info.Version),
1✔
882
        }
1✔
883
}
1✔
884

885
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
886
        var files []model.UpdateFile
1✔
887
        for _, u := range uFiles {
2✔
888
                files = append(files, model.UpdateFile{
1✔
889
                        Name:     u.Name,
1✔
890
                        Size:     u.Size,
1✔
891
                        Date:     &u.Date,
1✔
892
                        Checksum: string(u.Checksum),
1✔
893
                })
1✔
894
        }
1✔
895
        return files, nil
1✔
896
}
897

898
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
5✔
899
        metaArtifact := model.NewArtifactMeta()
5✔
900

5✔
901
        aReader := areader.NewReader(*r)
5✔
902

5✔
903
        // There is no signature verification here.
5✔
904
        // It is just simple check if artifact is signed or not.
5✔
905
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
5✔
906
                metaArtifact.Signed = true
×
907
                return nil
×
908
        }
×
909

910
        var err error
5✔
911
        if skipVerify {
8✔
912
                err = aReader.ReadArtifactHeaders()
3✔
913
                if err != nil {
5✔
914
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
915
                }
2✔
916
        } else {
3✔
917
                err = aReader.ReadArtifact()
3✔
918
                if err != nil {
6✔
919
                        return nil, errors.Wrap(err, "reading artifact error")
3✔
920
                }
3✔
921
        }
922

923
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
924
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
925

1✔
926
        metaArtifact.Name = aReader.GetArtifactName()
1✔
927
        if metaArtifact.Info.Version == 3 {
2✔
928
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
929
                if err != nil {
1✔
930
                        return nil, errors.Wrap(err,
×
931
                                "error parsing version 3 artifact")
×
932
                }
×
933

934
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
935
                if err != nil {
1✔
936
                        return nil, errors.Wrap(err,
×
937
                                "error parsing version 3 artifact")
×
938
                }
×
939

940
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
941
        }
942

943
        for _, p := range aReader.GetHandlers() {
2✔
944
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
945
                if err != nil {
1✔
946
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
947
                }
×
948

949
                uMetadata, err := p.GetUpdateMetaData()
1✔
950
                if err != nil {
1✔
951
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
952
                }
×
953

954
                metaArtifact.Updates = append(
1✔
955
                        metaArtifact.Updates,
1✔
956
                        model.Update{
1✔
957
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
958
                                        Type: p.GetUpdateType(),
1✔
959
                                },
1✔
960
                                Files:    uFiles,
1✔
961
                                MetaData: uMetadata,
1✔
962
                        })
1✔
963
        }
964

965
        return metaArtifact, nil
1✔
966
}
967

968
func getArtifactIDs(artifacts []*model.Image) []string {
7✔
969
        artifactIDs := make([]string, 0, len(artifacts))
7✔
970
        for _, artifact := range artifacts {
14✔
971
                artifactIDs = append(artifactIDs, artifact.Id)
7✔
972
        }
7✔
973
        return artifactIDs
7✔
974
}
975

976
// deployments
977
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
4✔
978
        ids := make([]string, len(devices))
4✔
979
        for i, d := range devices {
8✔
980
                ids[i] = d.ID
4✔
981
        }
4✔
982

983
        return ids
4✔
984
}
985

986
// updateDeploymentConstructor fills devices list with device ids
987
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
988
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
5✔
989
        l := log.FromContext(ctx)
5✔
990

5✔
991
        id := identity.FromContext(ctx)
5✔
992
        if id == nil {
5✔
993
                l.Error("identity not present in the context")
×
994
                return nil, ErrModelInternal
×
995
        }
×
996
        searchParams := model.SearchParams{
5✔
997
                Page:    1,
5✔
998
                PerPage: PerPageInventoryDevices,
5✔
999
                Filters: []model.FilterPredicate{
5✔
1000
                        {
5✔
1001
                                Scope:     InventoryIdentityScope,
5✔
1002
                                Attribute: InventoryStatusAttributeName,
5✔
1003
                                Type:      "$eq",
5✔
1004
                                Value:     InventoryStatusAccepted,
5✔
1005
                        },
5✔
1006
                },
5✔
1007
        }
5✔
1008
        if len(constructor.Group) > 0 {
10✔
1009
                searchParams.Filters = append(
5✔
1010
                        searchParams.Filters,
5✔
1011
                        model.FilterPredicate{
5✔
1012
                                Scope:     InventoryGroupScope,
5✔
1013
                                Attribute: InventoryGroupAttributeName,
5✔
1014
                                Type:      "$eq",
5✔
1015
                                Value:     constructor.Group,
5✔
1016
                        })
5✔
1017
        }
5✔
1018

1019
        for {
11✔
1020
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
6✔
1021
                if err != nil {
7✔
1022
                        l.Errorf("error searching for devices")
1✔
1023
                        return nil, ErrModelInternal
1✔
1024
                }
1✔
1025
                if count < 1 {
6✔
1026
                        l.Errorf("no devices found")
1✔
1027
                        return nil, ErrNoDevices
1✔
1028
                }
1✔
1029
                if len(devices) < 1 {
4✔
1030
                        break
×
1031
                }
1032
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
4✔
1033
                if len(constructor.Devices) == count {
7✔
1034
                        break
3✔
1035
                }
1036
                searchParams.Page++
1✔
1037
        }
1038

1039
        return constructor, nil
3✔
1040
}
1041

1042
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1043
func (d *Deployments) CreateDeviceConfigurationDeployment(
1044
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1045
        deviceID, deploymentID string) (string, error) {
5✔
1046

5✔
1047
        if constructor == nil {
6✔
1048
                return "", ErrModelMissingInput
1✔
1049
        }
1✔
1050

1051
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
4✔
1052
                constructor,
4✔
1053
                deploymentID,
4✔
1054
        )
4✔
1055
        if err != nil {
4✔
1056
                return "", errors.Wrap(err, "failed to create deployment")
×
1057
        }
×
1058

1059
        deployment.DeviceList = []string{deviceID}
4✔
1060
        deployment.MaxDevices = 1
4✔
1061
        deployment.Configuration = []byte(constructor.Configuration)
4✔
1062
        deployment.Type = model.DeploymentTypeConfiguration
4✔
1063

4✔
1064
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
4✔
1065
        if err != nil {
5✔
1066
                return "", err
1✔
1067
        }
1✔
1068
        deployment.Groups = groups
3✔
1069

3✔
1070
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
5✔
1071
                if strings.Contains(err.Error(), "duplicate key error") {
3✔
1072
                        return "", ErrDuplicateDeployment
1✔
1073
                }
1✔
1074
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
3✔
1075
                        return "", ErrInvalidDeploymentID
1✔
1076
                }
1✔
1077
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1078
        }
1079

1080
        return deployment.Id, nil
2✔
1081
}
1082

1083
// CreateDeployment precomputes new deployment and schedules it for devices.
1084
func (d *Deployments) CreateDeployment(ctx context.Context,
1085
        constructor *model.DeploymentConstructor) (string, error) {
9✔
1086

9✔
1087
        var err error
9✔
1088

9✔
1089
        if constructor == nil {
10✔
1090
                return "", ErrModelMissingInput
1✔
1091
        }
1✔
1092

1093
        if err := constructor.Validate(); err != nil {
8✔
1094
                return "", errors.Wrap(err, "Validating deployment")
×
1095
        }
×
1096

1097
        if len(constructor.Group) > 0 || constructor.AllDevices {
13✔
1098
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
5✔
1099
                if err != nil {
7✔
1100
                        return "", err
2✔
1101
                }
2✔
1102
        }
1103

1104
        deployment, err := model.NewDeploymentFromConstructor(constructor)
6✔
1105
        if err != nil {
6✔
1106
                return "", errors.Wrap(err, "failed to create deployment")
×
1107
        }
×
1108

1109
        // Assign artifacts to the deployment.
1110
        // When new artifact(s) with the artifact name same as the one in the deployment
1111
        // will be uploaded to the backend, it will also become part of this deployment.
1112
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
6✔
1113
        if err != nil {
6✔
1114
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1115
        }
×
1116

1117
        if len(artifacts) == 0 {
7✔
1118
                return "", ErrNoArtifact
1✔
1119
        }
1✔
1120

1121
        deployment.Artifacts = getArtifactIDs(artifacts)
6✔
1122
        deployment.DeviceList = constructor.Devices
6✔
1123
        deployment.MaxDevices = len(constructor.Devices)
6✔
1124
        deployment.Type = model.DeploymentTypeSoftware
6✔
1125
        if len(constructor.Group) > 0 {
9✔
1126
                deployment.Groups = []string{constructor.Group}
3✔
1127
        }
3✔
1128

1129
        // single device deployment case
1130
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
9✔
1131
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
3✔
1132
                if err != nil {
3✔
1133
                        return "", err
×
1134
                }
×
1135
                deployment.Groups = groups
3✔
1136
        }
1137

1138
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
7✔
1139
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1140
        }
1✔
1141

1142
        return deployment.Id, nil
5✔
1143
}
1144

1145
func (d *Deployments) getDeploymentGroups(
1146
        ctx context.Context,
1147
        devices []string,
1148
) ([]string, error) {
6✔
1149
        id := identity.FromContext(ctx)
6✔
1150

6✔
1151
        //only for single device deployment case
6✔
1152
        if len(devices) != 1 {
6✔
1153
                return nil, nil
×
1154
        }
×
1155

1156
        if id == nil {
7✔
1157
                id = &identity.Identity{}
1✔
1158
        }
1✔
1159

1160
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
6✔
1161
        if err != nil && err != inventory.ErrDevNotFound {
7✔
1162
                return nil, err
1✔
1163
        }
1✔
1164
        return groups, nil
5✔
1165
}
1166

1167
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1168
func (d *Deployments) IsDeploymentFinished(
1169
        ctx context.Context,
1170
        deploymentID string,
1171
) (bool, error) {
1✔
1172
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1173
        if err != nil {
1✔
1174
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1175
        }
×
1176
        if deployment == nil {
2✔
1177
                return true, nil
1✔
1178
        }
1✔
1179

1180
        return false, nil
1✔
1181
}
1182

1183
// GetDeployment fetches deployment by ID
1184
func (d *Deployments) GetDeployment(ctx context.Context,
1185
        deploymentID string) (*model.Deployment, error) {
1✔
1186

1✔
1187
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1188
        if err != nil {
1✔
1189
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1190
        }
×
1191

1192
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1193
                return nil, err
×
1194
        }
×
1195

1196
        return deployment, nil
1✔
1197
}
1198

1199
// ImageUsedInActiveDeployment checks if specified image is in use by deployments Image is
1200
// considered to be in use if it's participating in at lest one non success/error deployment.
1201
func (d *Deployments) ImageUsedInActiveDeployment(ctx context.Context,
1202
        imageID string) (bool, error) {
4✔
1203

4✔
1204
        var found bool
4✔
1205

4✔
1206
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
4✔
1207
        if err != nil {
5✔
1208
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1209
        }
1✔
1210

1211
        if found {
4✔
1212
                return found, nil
1✔
1213
        }
1✔
1214

1215
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
3✔
1216
                imageID, model.ActiveDeploymentStatuses()...)
3✔
1217
        if err != nil {
4✔
1218
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1219
        }
1✔
1220

1221
        return found, nil
2✔
1222
}
1223

1224
// ImageUsedInDeployment checks if specified image is in use by deployments
1225
// Image is considered to be in use if it's participating in any deployment.
1226
func (d *Deployments) ImageUsedInDeployment(ctx context.Context, imageID string) (bool, error) {
×
1227

×
1228
        var found bool
×
1229

×
1230
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1231
        if err != nil {
×
1232
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1233
        }
×
1234

1235
        if found {
×
1236
                return found, nil
×
1237
        }
×
1238

1239
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
1240
        if err != nil {
×
1241
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
1242
        }
×
1243

1244
        return found, nil
×
1245
}
1246

1247
// Retrieves the model.Deployment and model.DeviceDeployment structures
1248
// for the device. Upon error, nil is returned for both deployment structures.
1249
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1250
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1251

2✔
1252
        // Retrieve device deployment
2✔
1253
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
2✔
1254

2✔
1255
        if err != nil {
2✔
1256
                return nil, nil, errors.Wrap(err,
×
1257
                        "Searching for oldest active deployment for the device")
×
1258
        } else if deviceDeployment == nil {
3✔
1259
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1260
        }
1✔
1261

1262
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1263
        if err != nil {
2✔
1264
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1265
        }
×
1266
        if deployment == nil {
2✔
1267
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1268
        }
×
1269

1270
        return deployment, deviceDeployment, nil
2✔
1271
}
1272

1273
// getNewDeploymentForDevice returns deployment object and creates and returns
1274
// new device deployment for the device;
1275
//
1276
// we are interested only in the deployments that are newer than the latest
1277
// deployment applied by the device;
1278
// this way we guarantee that the device will not receive deployment
1279
// that is older than the one installed on the device;
1280
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1281
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1282

1✔
1283
        var lastDeployment *time.Time
1✔
1284
        //get latest device deployment for the device;
1✔
1285
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1286
        if err != nil {
1✔
1287
                return nil, nil, errors.Wrap(err,
×
1288
                        "Searching for latest active deployment for the device")
×
1289
        } else if deviceDeployment == nil {
2✔
1290
                lastDeployment = &time.Time{}
1✔
1291
        } else {
2✔
1292
                lastDeployment = deviceDeployment.Created
1✔
1293
        }
1✔
1294

1295
        //get deployments newer then last device deployment
1296
        //iterate over deployments and check if the device is part of the deployment or not
1297
        for skip := 0; true; skip += 100 {
2✔
1298
                deployments, err := d.db.FindNewerActiveDeployments(ctx, lastDeployment, skip, 100)
1✔
1299
                if err != nil {
1✔
1300
                        return nil, nil, errors.Wrap(err,
×
1301
                                "Failed to search for newer active deployments")
×
1302
                }
×
1303
                if len(deployments) == 0 {
2✔
1304
                        return nil, nil, nil
1✔
1305
                }
1✔
1306

1307
                for _, deployment := range deployments {
2✔
1308
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1309
                        if err != nil {
1✔
1310
                                return nil, nil, err
×
1311
                        }
×
1312
                        if ok {
2✔
1313
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1314
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1315
                                if err != nil {
1✔
1316
                                        return nil, nil, err
×
1317
                                }
×
1318
                                return deployment, deviceDeployment, nil
1✔
1319
                        }
1320
                }
1321
        }
1322

1323
        return nil, nil, nil
×
1324
}
1325

1326
func (d *Deployments) createDeviceDeploymentWithStatus(
1327
        ctx context.Context, deviceID string,
1328
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1329
) (*model.DeviceDeployment, error) {
6✔
1330
        prevStatus := model.DeviceDeploymentStatusNull
6✔
1331
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
6✔
1332
        if err != nil && err != mongo.ErrStorageNotFound {
6✔
1333
                return nil, err
×
1334
        } else if deviceDeployment != nil {
6✔
1335
                prevStatus = deviceDeployment.Status
×
1336
        }
×
1337

1338
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
6✔
1339
        deviceDeployment.Status = status
6✔
1340
        deviceDeployment.Active = status.Active()
6✔
1341
        deviceDeployment.Created = deployment.Created
6✔
1342

6✔
1343
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
6✔
1344
                return nil, err
×
1345
        }
×
1346

1347
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
6✔
1348
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
6✔
1349
                return nil, err
×
1350
        }
×
1351

1352
        // after inserting new device deployment update deployment stats
1353
        // in the database and locally, and update deployment status
1354
        if err := d.db.UpdateStatsInc(
6✔
1355
                ctx, deployment.Id,
6✔
1356
                prevStatus, status,
6✔
1357
        ); err != nil {
6✔
1358
                return nil, err
×
1359
        }
×
1360

1361
        deployment.Stats.Inc(status)
6✔
1362

6✔
1363
        err = d.recalcDeploymentStatus(ctx, deployment)
6✔
1364
        if err != nil {
6✔
1365
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1366
        }
×
1367

1368
        if !status.Active() {
11✔
1369
                err := d.reindexDevice(ctx, deviceID)
5✔
1370
                if err != nil {
5✔
1371
                        l := log.FromContext(ctx)
×
1372
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1373
                }
×
1374
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1375
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1376
                        l := log.FromContext(ctx)
×
1377
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1378
                }
×
1379
        }
1380

1381
        return deviceDeployment, nil
6✔
1382
}
1383

1384
func (d *Deployments) isDevicePartOfDeployment(
1385
        ctx context.Context,
1386
        deviceID string,
1387
        deployment *model.Deployment,
1388
) (bool, error) {
8✔
1389
        for _, id := range deployment.DeviceList {
14✔
1390
                if id == deviceID {
12✔
1391
                        return true, nil
6✔
1392
                }
6✔
1393
        }
1394
        return false, nil
3✔
1395
}
1396

1397
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1398
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1399
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
2✔
1400

2✔
1401
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
2✔
1402
        if err != nil {
2✔
1403
                return nil, ErrModelInternal
×
1404
        } else if deployment == nil {
3✔
1405
                return nil, nil
1✔
1406
        }
1✔
1407

1408
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
2✔
1409
        if err != nil {
3✔
1410
                return nil, err
1✔
1411
        }
1✔
1412
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
2✔
1413
}
1414

1415
func (d *Deployments) getDeploymentInstructions(
1416
        ctx context.Context,
1417
        deployment *model.Deployment,
1418
        deviceDeployment *model.DeviceDeployment,
1419
        request *model.DeploymentNextRequest,
1420
) (*model.DeploymentInstructions, error) {
2✔
1421

2✔
1422
        var newArtifactAssigned bool
2✔
1423

2✔
1424
        l := log.FromContext(ctx)
2✔
1425

2✔
1426
        if deployment.Type == model.DeploymentTypeConfiguration {
3✔
1427
                // There's nothing more we need to do, the link must be filled
1✔
1428
                // in by the API layer.
1✔
1429
                return &model.DeploymentInstructions{
1✔
1430
                        ID: deployment.Id,
1✔
1431
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1432
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1433
                                // use deployment ID togheter with device ID as artifact ID
1✔
1434
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1435
                                ArtifactName:          deployment.ArtifactName,
1✔
1436
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1437
                        },
1✔
1438
                        Type: model.DeploymentTypeConfiguration,
1✔
1439
                }, nil
1✔
1440
        }
1✔
1441

1442
        // assing artifact to the device deployment
1443
        // only if it was not assgined previously
1444
        if deviceDeployment.Image == nil {
4✔
1445
                if err := d.assignArtifact(
2✔
1446
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
2✔
1447
                        return nil, err
×
1448
                }
×
1449
                newArtifactAssigned = true
2✔
1450
        }
1451

1452
        if deviceDeployment.Image == nil {
2✔
1453
                // No artifact - return empty response
×
1454
                return nil, nil
×
1455
        }
×
1456

1457
        // if the deployment is not forcing the installation, and
1458
        // if artifact was recognized as already installed, and this is
1459
        // a new device deployment - indicated by device deployment status "pending",
1460
        // handle already installed artifact case
1461
        if !deployment.ForceInstallation &&
2✔
1462
                d.isAlreadyInstalled(request, deviceDeployment) &&
2✔
1463
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
4✔
1464
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
2✔
1465
        }
2✔
1466

1467
        // if new artifact has been assigned to device deployment
1468
        // add artifact size to deployment total size,
1469
        // before returning deployment instruction to the device
1470
        if newArtifactAssigned {
2✔
1471
                if err := d.db.IncrementDeploymentTotalSize(
1✔
1472
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
1473
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1474
                }
×
1475
        }
1476

1477
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1478
        if err != nil {
1✔
1479
                return nil, err
×
1480
        }
×
1481

1482
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1483
        link, err := d.objectStorage.GetRequest(
1✔
1484
                ctx,
1✔
1485
                imagePath,
1✔
1486
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1487
                DefaultUpdateDownloadLinkExpire,
1✔
1488
        )
1✔
1489
        if err != nil {
1✔
1490
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1491
        }
×
1492

1493
        instructions := &model.DeploymentInstructions{
1✔
1494
                ID: deviceDeployment.DeploymentId,
1✔
1495
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1496
                        ID: deviceDeployment.Image.Id,
1✔
1497
                        ArtifactName: deviceDeployment.Image.
1✔
1498
                                ArtifactMeta.Name,
1✔
1499
                        Source: *link,
1✔
1500
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1501
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1502
                },
1✔
1503
        }
1✔
1504

1✔
1505
        return instructions, nil
1✔
1506
}
1507

1508
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1509
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
2✔
1510
        if deviceDeployment.Request != nil {
3✔
1511
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1512
                        // the device reported different device type and/or artifact name
1✔
1513
                        // during the update process, which should never happen;
1✔
1514
                        // mark deployment for this device as failed to force client to rollback
1✔
1515
                        l := log.FromContext(ctx)
1✔
1516
                        l.Errorf(
1✔
1517
                                "Device with id %s reported new data: %s during update process;"+
1✔
1518
                                        "old data: %s",
1✔
1519
                                deviceID, request, deviceDeployment.Request)
1✔
1520

1✔
1521
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1522
                                model.DeviceDeploymentState{
1✔
1523
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1524
                                }); err != nil {
1✔
1525
                                return errors.Wrap(err, "Failed to update deployment status")
×
1526
                        }
×
1527
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1528
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1529
                        }
×
1530
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1531
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1532
                                l := log.FromContext(ctx)
×
1533
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1534
                        }
×
1535
                        return ErrConflictingRequestData
1✔
1536
                }
1537
        } else {
2✔
1538
                // save the request
2✔
1539
                if err := d.db.SaveDeviceDeploymentRequest(
2✔
1540
                        ctx, deviceDeployment.Id, request); err != nil {
2✔
1541
                        return err
×
1542
                }
×
1543
        }
1544
        return nil
2✔
1545
}
1546

1547
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1548
// ID `deviceID`. Returns nil if update was successful.
1549
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1550
        deviceID string, ddState model.DeviceDeploymentState) error {
6✔
1551

6✔
1552
        l := log.FromContext(ctx)
6✔
1553

6✔
1554
        l.Infof("New status: %s for device %s deployment: %v", ddState.Status, deviceID, deploymentID)
6✔
1555

6✔
1556
        var finishTime *time.Time = nil
6✔
1557
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
10✔
1558
                now := time.Now()
4✔
1559
                finishTime = &now
4✔
1560
        }
4✔
1561

1562
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
6✔
1563
        if err == mongo.ErrStorageNotFound {
7✔
1564
                return ErrStorageNotFound
1✔
1565
        } else if err != nil {
6✔
1566
                return err
×
1567
        }
×
1568

1569
        currentStatus := dd.Status
5✔
1570

5✔
1571
        if currentStatus == model.DeviceDeploymentStatusAborted {
5✔
1572
                return ErrDeploymentAborted
×
1573
        }
×
1574

1575
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
5✔
1576
                return ErrDeviceDecommissioned
×
1577
        }
×
1578

1579
        // nothing to do
1580
        if ddState.Status == currentStatus {
5✔
1581
                return nil
×
1582
        }
×
1583

1584
        // update finish time
1585
        ddState.FinishTime = finishTime
5✔
1586

5✔
1587
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
5✔
1588
                deviceID, deploymentID, ddState)
5✔
1589
        if err != nil {
5✔
1590
                return err
×
1591
        }
×
1592

1593
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
5✔
1594
                return err
×
1595
        }
×
1596

1597
        // fetch deployment stats and update deployment status
1598
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
1599
        if err != nil {
5✔
1600
                return errors.Wrap(err, "failed when searching for deployment")
×
1601
        }
×
1602

1603
        err = d.recalcDeploymentStatus(ctx, deployment)
5✔
1604
        if err != nil {
5✔
1605
                return errors.Wrap(err, "failed to update deployment status")
×
1606
        }
×
1607

1608
        if !ddState.Status.Active() {
9✔
1609
                l := log.FromContext(ctx)
4✔
1610
                ldd := model.DeviceDeployment{
4✔
1611
                        DeviceId:     dd.DeviceId,
4✔
1612
                        DeploymentId: dd.DeploymentId,
4✔
1613
                        Id:           dd.Id,
4✔
1614
                        Status:       ddState.Status,
4✔
1615
                }
4✔
1616
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
4✔
1617
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1618
                }
×
1619
                if err := d.reindexDevice(ctx, deviceID); err != nil {
4✔
1620
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1621
                }
×
1622
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
4✔
1623
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1624
                }
×
1625
        }
1626

1627
        return nil
5✔
1628
}
1629

1630
// recalcDeploymentStatus inspects the deployment stats and
1631
// recalculates and updates its status
1632
// it should be used whenever deployment stats are touched
1633
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
10✔
1634
        status := dep.GetStatus()
10✔
1635

10✔
1636
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
10✔
1637
                return err
×
1638
        }
×
1639

1640
        return nil
10✔
1641
}
1642

1643
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1644
        deploymentID string) (model.Stats, error) {
1✔
1645

1✔
1646
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1647

1✔
1648
        if err != nil {
1✔
1649
                return nil, errors.Wrap(err, "checking deployment id")
×
1650
        }
×
1651

1652
        if deployment == nil {
1✔
1653
                return nil, nil
×
1654
        }
×
1655

1656
        return deployment.Stats, nil
1✔
1657
}
1658
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1659
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1660

×
1661
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1662

×
1663
        if err != nil {
×
1664
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1665
        }
×
1666

1667
        if deploymentStats == nil {
×
1668
                return nil, ErrModelDeploymentNotFound
×
1669
        }
×
1670

1671
        return deploymentStats, nil
×
1672
}
1673

1674
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1675
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1676
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1677

1✔
1678
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1679
        if err != nil {
1✔
1680
                return nil, ErrModelInternal
×
1681
        }
×
1682

1683
        if deployment == nil {
1✔
1684
                return nil, ErrModelDeploymentNotFound
×
1685
        }
×
1686

1687
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1688
        if err != nil {
1✔
1689
                return nil, ErrModelInternal
×
1690
        }
×
1691

1692
        return statuses, nil
1✔
1693
}
1694

1695
func (d *Deployments) GetDevicesListForDeployment(ctx context.Context,
1696
        query store.ListQuery) ([]model.DeviceDeployment, int, error) {
1✔
1697

1✔
1698
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1699
        if err != nil {
1✔
1700
                return nil, -1, ErrModelInternal
×
1701
        }
×
1702

1703
        if deployment == nil {
1✔
1704
                return nil, -1, ErrModelDeploymentNotFound
×
1705
        }
×
1706

1707
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1708
        if err != nil {
1✔
1709
                return nil, -1, ErrModelInternal
×
1710
        }
×
1711

1712
        return statuses, totalCount, nil
1✔
1713
}
1714

1715
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1716
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
4✔
1717
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
4✔
1718
        if err != nil {
5✔
1719
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1720
        }
1✔
1721

1722
        deploymentIDs := make([]string, len(deviceDeployments))
3✔
1723
        for i, deviceDeployment := range deviceDeployments {
9✔
1724
                deploymentIDs[i] = deviceDeployment.DeploymentId
6✔
1725
        }
6✔
1726

1727
        deployments, _, err := d.db.Find(ctx, model.Query{
3✔
1728
                IDs:          deploymentIDs,
3✔
1729
                Limit:        len(deviceDeployments),
3✔
1730
                DisableCount: true,
3✔
1731
        })
3✔
1732
        if err != nil {
4✔
1733
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1734
        }
1✔
1735

1736
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1737
        for _, deployment := range deployments {
5✔
1738
                deploymentsMap[deployment.Id] = deployment
3✔
1739
        }
3✔
1740

1741
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1742
        for i, deviceDeployment := range deviceDeployments {
6✔
1743
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
7✔
1744
                        res = append(res, model.DeviceDeploymentListItem{
3✔
1745
                                Id:         deviceDeployment.Id,
3✔
1746
                                Deployment: deployment,
3✔
1747
                                Device:     &deviceDeployments[i],
3✔
1748
                        })
3✔
1749
                } else {
4✔
1750
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1751
                                Id:     deviceDeployment.Id,
1✔
1752
                                Device: &deviceDeployments[i],
1✔
1753
                        })
1✔
1754
                }
1✔
1755
        }
1756

1757
        return res, totalCount, nil
2✔
1758
}
1759

1760
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1761
        ctx context.Context,
1762
        deployment *model.Deployment,
1763
) error {
6✔
1764
        if deployment.DeviceCount == nil {
6✔
1765
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1766
                if err != nil {
×
1767
                        return errors.Wrap(err, "counting device deployments")
×
1768
                }
×
1769
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1770
                if err != nil {
×
1771
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1772
                }
×
1773
                deployment.DeviceCount = &deviceCount
×
1774
        }
1775

1776
        return nil
6✔
1777
}
1778

1779
func (d *Deployments) LookupDeployment(ctx context.Context,
1780
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1781
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1782

1✔
1783
        if err != nil {
1✔
1784
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1785
        }
×
1786

1787
        if list == nil {
2✔
1788
                return make([]*model.Deployment, 0), 0, nil
1✔
1789
        }
1✔
1790

1791
        for _, deployment := range list {
×
1792
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1793
                        return nil, 0, err
×
1794
                }
×
1795
        }
1796

1797
        return list, totalCount, nil
×
1798
}
1799

1800
// SaveDeviceDeploymentLog will save the deployment log for device of
1801
// ID `deviceID`. Returns nil if log was saved successfully.
1802
func (d *Deployments) SaveDeviceDeploymentLog(ctx context.Context, deviceID string,
1803
        deploymentID string, logs []model.LogMessage) error {
1✔
1804

1✔
1805
        // repack to temporary deployment log and validate
1✔
1806
        dlog := model.DeploymentLog{
1✔
1807
                DeviceID:     deviceID,
1✔
1808
                DeploymentID: deploymentID,
1✔
1809
                Messages:     logs,
1✔
1810
        }
1✔
1811
        if err := dlog.Validate(); err != nil {
1✔
1812
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1813
        }
×
1814

1815
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1816
                if err != nil {
×
1817
                        return err
×
1818
                } else {
×
1819
                        return ErrModelDeploymentNotFound
×
1820
                }
×
1821
        }
1822

1823
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1824
                return err
×
1825
        }
×
1826

1827
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1828
                deviceID, deploymentID, true)
1✔
1829
}
1830

1831
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1832
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1833

1✔
1834
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1835
                deviceID, deploymentID)
1✔
1836
}
1✔
1837

1838
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1839
        deploymentID string, deviceID string) (bool, error) {
1✔
1840
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1841
}
1✔
1842

1843
// AbortDeployment aborts deployment for devices and updates deployment stats
1844
func (d *Deployments) AbortDeployment(ctx context.Context, deploymentID string) error {
5✔
1845

5✔
1846
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
6✔
1847
                return err
1✔
1848
        }
1✔
1849

1850
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
4✔
1851
                ctx, deploymentID)
4✔
1852
        if err != nil {
5✔
1853
                return err
1✔
1854
        }
1✔
1855

1856
        // update statistics
1857
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
4✔
1858
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1859
        }
1✔
1860

1861
        // when aborting the deployment we need to set status directly instead of
1862
        // using recalcDeploymentStatus method;
1863
        // it is possible that the deployment does not have any device deployments yet;
1864
        // in that case, all statistics are 0 and calculating status based on statistics
1865
        // will not work - the calculated status will be "pending"
1866
        if err := d.db.SetDeploymentStatus(ctx,
2✔
1867
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
2✔
1868
                return errors.Wrap(err, "failed to update deployment status")
×
1869
        }
×
1870

1871
        return nil
2✔
1872
}
1873

1874
func (d *Deployments) updateDeviceDeploymentsStatus(
1875
        ctx context.Context,
1876
        deviceId string,
1877
        status model.DeviceDeploymentStatus,
1878
) error {
15✔
1879
        var latestDeployment *time.Time
15✔
1880
        // Retrieve active device deployment for the device
15✔
1881
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
15✔
1882
        if err != nil {
17✔
1883
                return errors.Wrap(err, "Searching for active deployment for the device")
2✔
1884
        } else if deviceDeployment != nil {
17✔
1885
                now := time.Now()
2✔
1886
                ddStatus := model.DeviceDeploymentState{
2✔
1887
                        Status:     status,
2✔
1888
                        FinishTime: &now,
2✔
1889
                }
2✔
1890
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
2✔
1891
                        deviceId, ddStatus); err != nil {
2✔
1892
                        return errors.Wrap(err, "updating device deployment status")
×
1893
                }
×
1894
                latestDeployment = deviceDeployment.Created
2✔
1895
        } else {
11✔
1896
                // get latest device deployment for the device
11✔
1897
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
11✔
1898
                if err != nil {
11✔
1899
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1900
                } else if deviceDeployment == nil {
20✔
1901
                        latestDeployment = &time.Time{}
9✔
1902
                } else {
11✔
1903
                        latestDeployment = deviceDeployment.Created
2✔
1904
                }
2✔
1905
        }
1906

1907
        // get deployments newer then last device deployment
1908
        // iterate over deployments and check if the device is part of the deployment or not
1909
        // if the device is part of the deployment create new, decommisioned device deployment
1910
        for skip := 0; true; skip += 100 {
33✔
1911
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
20✔
1912
                if err != nil {
20✔
1913
                        return errors.Wrap(err, "Failed to search for newer active deployments")
×
1914
                }
×
1915
                if len(deployments) == 0 {
33✔
1916
                        break
13✔
1917
                }
1918
                for _, deployment := range deployments {
14✔
1919
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
7✔
1920
                        if err != nil {
7✔
1921
                                return err
×
1922
                        }
×
1923
                        if ok {
12✔
1924
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
5✔
1925
                                        deviceId, deployment, status)
5✔
1926
                                if err != nil {
5✔
1927
                                        return err
×
1928
                                }
×
1929
                                if !status.Active() {
10✔
1930
                                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1931
                                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1932
                                                l := log.FromContext(ctx)
×
1933
                                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1934
                                        }
×
1935
                                }
1936
                        }
1937
                }
1938
        }
1939

1940
        if err := d.reindexDevice(ctx, deviceId); err != nil {
13✔
1941
                l := log.FromContext(ctx)
×
1942
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1943
        }
×
1944

1945
        return nil
13✔
1946
}
1947

1948
// DecommissionDevice updates the status of all the pending and active deployments for a device
1949
// to decommissioned
1950
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
7✔
1951
        return d.updateDeviceDeploymentsStatus(
7✔
1952
                ctx,
7✔
1953
                deviceId,
7✔
1954
                model.DeviceDeploymentStatusDecommissioned,
7✔
1955
        )
7✔
1956
}
7✔
1957

1958
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1959
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
8✔
1960
        return d.updateDeviceDeploymentsStatus(
8✔
1961
                ctx,
8✔
1962
                deviceId,
8✔
1963
                model.DeviceDeploymentStatusAborted,
8✔
1964
        )
8✔
1965
}
8✔
1966

1967
// DeleteDeviceDeploymentsHistory deletes the device deployments history
1968
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
2✔
1969
        // get device deployments which will be marked as deleted
2✔
1970
        f := false
2✔
1971
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
2✔
1972
        if err != nil {
2✔
1973
                return err
×
1974
        }
×
1975

1976
        // no device deployments to update
1977
        if len(dd) <= 0 {
2✔
1978
                return nil
×
1979
        }
×
1980

1981
        // mark device deployments as deleted
1982
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
3✔
1983
                return err
1✔
1984
        }
1✔
1985

1986
        // trigger reindexing of updated device deployments
1987
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
1988
        for i, d := range dd {
2✔
1989
                deviceDeployments[i].ID = d.Id
1✔
1990
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
1991
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
1992
        }
1✔
1993
        return d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
1994
}
1995

1996
// Storage settings
1997
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
1998
        settings, err := d.db.GetStorageSettings(ctx)
3✔
1999
        if err != nil {
4✔
2000
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2001
        }
1✔
2002

2003
        return settings, nil
2✔
2004
}
2005

2006
func (d *Deployments) SetStorageSettings(
2007
        ctx context.Context,
2008
        storageSettings *model.StorageSettings,
2009
) error {
4✔
2010
        if storageSettings != nil {
8✔
2011
                ctx = storage.SettingsWithContext(ctx, storageSettings)
4✔
2012
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
4✔
UNCOV
2013
                        return errors.WithMessage(err,
×
UNCOV
2014
                                "the provided storage settings failed the health check",
×
UNCOV
2015
                        )
×
2016
                }
×
2017
        }
2018
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
6✔
2019
                return errors.Wrap(err, "Failed to save settings")
2✔
2020
        }
2✔
2021

2022
        return nil
2✔
2023
}
2024

2025
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
8✔
2026
        d.reportingClient = c
8✔
2027
        return d
8✔
2028
}
8✔
2029

2030
func (d *Deployments) haveReporting() bool {
6✔
2031
        return d.reportingClient != nil
6✔
2032
}
6✔
2033

2034
func (d *Deployments) search(
2035
        ctx context.Context,
2036
        tid string,
2037
        parms model.SearchParams,
2038
) ([]model.InvDevice, int, error) {
6✔
2039
        if d.haveReporting() {
7✔
2040
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2041
        } else {
6✔
2042
                return d.inventoryClient.Search(ctx, tid, parms)
5✔
2043
        }
5✔
2044
}
2045

2046
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2047
        ctx context.Context,
2048
        artifactName string,
2049
) error {
2✔
2050
        // first check if there are pending deployments with given artifact name
2✔
2051
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
2✔
2052
        if err != nil {
2✔
UNCOV
2053
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
UNCOV
2054
        }
×
2055
        if !exists {
3✔
2056
                return nil
1✔
2057
        }
1✔
2058

2059
        // Assign artifacts to the deployments with given artifact name
2060
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2061
        if err != nil {
1✔
UNCOV
2062
                return errors.Wrap(err, "Finding artifact with given name")
×
UNCOV
2063
        }
×
2064

2065
        if len(artifacts) == 0 {
1✔
2066
                return ErrNoArtifact
×
UNCOV
2067
        }
×
2068
        artifactIDs := getArtifactIDs(artifacts)
1✔
2069
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2070
}
2071

2072
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
25✔
2073
        if d.reportingClient != nil {
28✔
2074
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
3✔
2075
        }
3✔
2076
        return nil
22✔
2077
}
2078

2079
func (d *Deployments) reindexDeployment(ctx context.Context,
2080
        deviceID, deploymentID, ID string) error {
17✔
2081
        if d.reportingClient != nil {
20✔
2082
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
3✔
2083
        }
3✔
2084
        return nil
14✔
2085
}
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