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

mendersoftware / deployments / 890874106

pending completion
890874106

Pull #867

gitlab-ci

kjaskiewiczz
feat: make releases persistent in the database

Changelog: Title
Ticket: MEN-5180

Signed-off-by: Krzysztof Jaskiewicz <krzysztof.jaskiewicz@northern.tech>
Pull Request #867: feat: make releases persistent in the database

81 of 239 new or added lines in 5 files covered. (33.89%)

1 existing line in 1 file now uncovered.

7174 of 9174 relevant lines covered (78.2%)

67.85 hits per line

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

76.6
/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

191
type Deployments struct {
192
        db              store.DataStore
193
        objectStorage   storage.ObjectStorage
194
        workflowsClient workflows.Client
195
        inventoryClient inventory.Client
196
        reportingClient reporting.Client
197
}
198

199
// Compile-time check
200
var _ App = &Deployments{}
201

202
func NewDeployments(
203
        storage store.DataStore,
204
        objectStorage storage.ObjectStorage,
205
) *Deployments {
125✔
206
        return &Deployments{
125✔
207
                db:              storage,
125✔
208
                objectStorage:   objectStorage,
125✔
209
                workflowsClient: workflows.NewClient(),
125✔
210
                inventoryClient: inventory.NewClient(),
125✔
211
        }
125✔
212
}
125✔
213

214
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
8✔
215
        d.workflowsClient = workflowsClient
8✔
216
}
8✔
217

218
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
16✔
219
        d.inventoryClient = inventoryClient
16✔
220
}
16✔
221

222
func (d *Deployments) HealthCheck(ctx context.Context) error {
12✔
223
        err := d.db.Ping(ctx)
12✔
224
        if err != nil {
14✔
225
                return errors.Wrap(err, "error reaching MongoDB")
2✔
226
        }
2✔
227
        err = d.objectStorage.HealthCheck(ctx)
10✔
228
        if err != nil {
12✔
229
                return errors.Wrap(
2✔
230
                        err,
2✔
231
                        "error reaching artifact storage service",
2✔
232
                )
2✔
233
        }
2✔
234

235
        err = d.workflowsClient.CheckHealth(ctx)
8✔
236
        if err != nil {
10✔
237
                return errors.Wrap(err, "Workflows service unhealthy")
2✔
238
        }
2✔
239

240
        err = d.inventoryClient.CheckHealth(ctx)
6✔
241
        if err != nil {
8✔
242
                return errors.Wrap(err, "Inventory service unhealthy")
2✔
243
        }
2✔
244

245
        if d.reportingClient != nil {
8✔
246
                err = d.reportingClient.CheckHealth(ctx)
4✔
247
                if err != nil {
6✔
248
                        return errors.Wrap(err, "Reporting service unhealthy")
2✔
249
                }
2✔
250
        }
251
        return nil
2✔
252
}
253

254
func (d *Deployments) contextWithStorageSettings(
255
        ctx context.Context,
256
) (context.Context, error) {
51✔
257
        var err error
51✔
258
        settings, ok := storage.SettingsFromContext(ctx)
51✔
259
        if !ok {
94✔
260
                settings, err = d.db.GetStorageSettings(ctx)
43✔
261
        }
43✔
262
        if err != nil {
55✔
263
                return nil, err
4✔
264
        } else if settings != nil {
51✔
265
                err = settings.Validate()
×
266
                if err != nil {
×
267
                        return nil, err
×
268
                }
×
269
        }
270
        return storage.SettingsWithContext(ctx, settings), nil
47✔
271
}
272

273
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
6✔
274
        limit, err := d.db.GetLimit(ctx, name)
6✔
275
        if err == mongo.ErrLimitNotFound {
8✔
276
                return &model.Limit{
2✔
277
                        Name:  name,
2✔
278
                        Value: 0,
2✔
279
                }, nil
2✔
280

2✔
281
        } else if err != nil {
8✔
282
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
2✔
283
        }
2✔
284
        return limit, nil
2✔
285
}
286

287
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
5✔
288
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
7✔
289
                return errors.Wrap(err, "failed to provision tenant")
2✔
290
        }
2✔
291

292
        return nil
3✔
293
}
294

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

303
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
304
// and creates image structure in the system.
305
// Returns image ID, artifact file ID and nil on success.
306
func (d *Deployments) handleArtifact(ctx context.Context,
307
        multipartUploadMsg *model.MultipartUploadMsg,
308
        skipVerify bool,
309
) (string, error) {
9✔
310

9✔
311
        l := log.FromContext(ctx)
9✔
312
        ctx, err := d.contextWithStorageSettings(ctx)
9✔
313
        if err != nil {
9✔
314
                return "", err
×
315
        }
×
316

317
        // create pipe
318
        pR, pW := io.Pipe()
9✔
319

9✔
320
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
9✔
321

9✔
322
        tee := io.TeeReader(artifactReader, pW)
9✔
323

9✔
324
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
9✔
325
        if err != nil {
10✔
326
                uid, _ = uuid.NewRandom()
1✔
327
        }
1✔
328
        artifactID := uid.String()
9✔
329

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

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

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

379
        // close the pipe
380
        pW.Close()
1✔
381

1✔
382
        // collect output from the goroutine
1✔
383
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
384
                return artifactID, uploadResponseErr
×
385
        }
×
386

387
        image := model.NewImage(
1✔
388
                artifactID,
1✔
389
                multipartUploadMsg.MetaConstructor,
1✔
390
                metaArtifactConstructor,
1✔
391
                artifactReader.Count(),
1✔
392
        )
1✔
393

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

411
        // update release
412
        if err := d.updateRelease(ctx, image, nil); err != nil {
1✔
NEW
413
                return "", err
×
NEW
414
        }
×
415

416
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
417
                return "", errors.Wrap(err, "fail to update deployments")
×
418
        }
×
419

420
        return artifactID, nil
1✔
421
}
422

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

21✔
430
        if multipartGenerateImageMsg == nil {
23✔
431
                return "", ErrModelMultipartUploadMsgMalformed
2✔
432
        }
2✔
433

434
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
19✔
435
        if err != nil {
29✔
436
                return "", err
10✔
437
        }
10✔
438
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
11✔
439
                multipartGenerateImageMsg.TenantID = id.Tenant
2✔
440
        }
2✔
441
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
9✔
442
        if err != nil {
13✔
443
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
6✔
444
                        return "", errors.Wrap(err, cleanupErr.Error())
2✔
445
                }
2✔
446
                return "", err
2✔
447
        }
448

449
        return multipartGenerateImageMsg.ArtifactID, err
5✔
450
}
451

452
func (d *Deployments) GenerateConfigurationImage(
453
        ctx context.Context,
454
        deviceType string,
455
        deploymentID string,
456
) (io.Reader, error) {
9✔
457
        var buf bytes.Buffer
9✔
458
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
9✔
459
        if err != nil {
11✔
460
                return nil, err
2✔
461
        } else if dpl == nil {
11✔
462
                return nil, ErrModelDeploymentNotFound
2✔
463
        }
2✔
464
        var metaData map[string]interface{}
5✔
465
        err = json.Unmarshal(dpl.Configuration, &metaData)
5✔
466
        if err != nil {
7✔
467
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
2✔
468
        }
2✔
469

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

3✔
495
        return &buf, err
3✔
496
}
497

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

19✔
509
        // check if artifact is unique
19✔
510
        // artifact is considered to be unique if there is no artifact with the same name
19✔
511
        // and supporting the same platform in the system
19✔
512
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
19✔
513
                multipartMsg.Name,
19✔
514
                multipartMsg.DeviceTypesCompatible,
19✔
515
        )
19✔
516
        if err != nil {
21✔
517
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
2✔
518
        }
2✔
519
        if !isArtifactUnique {
19✔
520
                return "", ErrModelArtifactNotUnique
2✔
521
        }
2✔
522

523
        ctx, err = d.contextWithStorageSettings(ctx)
15✔
524
        if err != nil {
15✔
525
                return "", err
×
526
        }
×
527
        err = d.objectStorage.PutObject(
15✔
528
                ctx, filePath, multipartMsg.FileReader,
15✔
529
        )
15✔
530
        if err != nil {
17✔
531
                return "", err
2✔
532
        }
2✔
533
        defer func() {
26✔
534
                if err != nil {
17✔
535
                        e := d.objectStorage.DeleteObject(ctx, filePath)
4✔
536
                        if e != nil {
8✔
537
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
4✔
538
                                        filePath, e)
4✔
539
                        }
4✔
540
                }
541
        }()
542

543
        link, err := d.objectStorage.GetRequest(
13✔
544
                ctx,
13✔
545
                filePath,
13✔
546
                path.Base(filePath),
13✔
547
                DefaultImageGenerationLinkExpire,
13✔
548
        )
13✔
549
        if err != nil {
15✔
550
                return "", err
2✔
551
        }
2✔
552
        multipartMsg.GetArtifactURI = link.Uri
11✔
553

11✔
554
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
11✔
555
        if err != nil {
13✔
556
                return "", err
2✔
557
        }
2✔
558
        multipartMsg.DeleteArtifactURI = link.Uri
9✔
559

9✔
560
        return artifactID, nil
9✔
561
}
562

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

1✔
567
        image, err := d.db.FindImageByID(ctx, id)
1✔
568
        if err != nil {
1✔
569
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
570
        }
×
571

572
        if image == nil {
2✔
573
                return nil, nil
1✔
574
        }
1✔
575

576
        return image, nil
1✔
577
}
578

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

1✔
589
        if err != nil {
1✔
590
                return errors.Wrap(err, "Getting image metadata")
×
591
        }
×
592

593
        if found == nil {
1✔
594
                return ErrImageMetaNotFound
×
595
        }
×
596

597
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
598
        if err != nil {
1✔
599
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
600
        }
×
601

602
        // Image is in use, not allowed to delete
603
        if inUse {
2✔
604
                return ErrModelImageInActiveDeployment
1✔
605
        }
1✔
606

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

618
        // Delete metadata
619
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
620
                return errors.Wrap(err, "Deleting image metadata")
×
621
        }
×
622

623
        // update release
624
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
NEW
625
                return err
×
NEW
626
        }
×
627

628
        return nil
1✔
629
}
630

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

641
        if imageList == nil {
2✔
642
                return make([]*model.Image, 0), 0, nil
1✔
643
        }
1✔
644

645
        return imageList, count, nil
1✔
646
}
647

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

×
652
        if err := constructor.Validate(); err != nil {
×
653
                return false, errors.Wrap(err, "Validating image metadata")
×
654
        }
×
655

656
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
657
        if err != nil {
×
658
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
659
        }
×
660

661
        if found {
×
662
                return false, ErrModelImageUsedInAnyDeployment
×
663
        }
×
664

665
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
666
        if err != nil {
×
667
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
668
        }
×
669

670
        if foundImage == nil {
×
671
                return false, nil
×
672
        }
×
673

674
        foundImage.SetModified(time.Now())
×
675
        foundImage.ImageMeta = constructor
×
676

×
677
        _, err = d.db.Update(ctx, foundImage)
×
678
        if err != nil {
×
679
                return false, errors.Wrap(err, "Updating image matadata")
×
680
        }
×
681

NEW
682
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
NEW
683
                return false, err
×
NEW
684
        }
×
685

UNCOV
686
        return true, nil
×
687
}
688

689
// DownloadLink presigned GET link to download image file.
690
// Returns error if image have not been uploaded.
691
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
692
        expire time.Duration) (*model.Link, error) {
1✔
693

1✔
694
        image, err := d.GetImage(ctx, imageID)
1✔
695
        if err != nil {
1✔
696
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
697
        }
×
698

699
        if image == nil {
1✔
700
                return nil, nil
×
701
        }
×
702

703
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
704
        if err != nil {
1✔
705
                return nil, err
×
706
        }
×
707
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
708
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
709
        if err != nil {
1✔
710
                return nil, errors.Wrap(err, "Searching for image file")
×
711
        }
×
712

713
        link, err := d.objectStorage.GetRequest(
1✔
714
                ctx,
1✔
715
                imagePath,
1✔
716
                image.Name+model.ArtifactFileSuffix,
1✔
717
                expire,
1✔
718
        )
1✔
719
        if err != nil {
1✔
720
                return nil, errors.Wrap(err, "Generating download link")
×
721
        }
×
722

723
        return link, nil
1✔
724
}
725

726
func (d *Deployments) UploadLink(
727
        ctx context.Context,
728
        expire time.Duration,
729
        skipVerify bool,
730
) (*model.UploadLink, error) {
11✔
731
        ctx, err := d.contextWithStorageSettings(ctx)
11✔
732
        if err != nil {
13✔
733
                return nil, err
2✔
734
        }
2✔
735

736
        artifactID := uuid.New().String()
9✔
737
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
9✔
738
        if skipVerify {
10✔
739
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
740
        }
1✔
741
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
9✔
742
        if err != nil {
11✔
743
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
2✔
744
        }
2✔
745
        upLink := &model.UploadLink{
7✔
746
                ArtifactID: artifactID,
7✔
747
                IssuedAt:   time.Now(),
7✔
748
                Link:       *link,
7✔
749
        }
7✔
750
        err = d.db.InsertUploadIntent(ctx, upLink)
7✔
751
        if err != nil {
9✔
752
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
2✔
753
        }
2✔
754

755
        return upLink, err
5✔
756
}
757

758
func (d *Deployments) processUploadedArtifact(
759
        ctx context.Context,
760
        artifactID string,
761
        artifact io.ReadCloser,
762
        skipVerify bool,
763
) error {
9✔
764
        linkStatus := model.LinkStatusCompleted
9✔
765

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

813
func (d *Deployments) CompleteUpload(
814
        ctx context.Context,
815
        intentID string,
816
        skipVerify bool,
817
) error {
19✔
818
        l := log.FromContext(ctx)
19✔
819
        idty := identity.FromContext(ctx)
19✔
820
        ctx, err := d.contextWithStorageSettings(ctx)
19✔
821
        if err != nil {
21✔
822
                return err
2✔
823
        }
2✔
824
        // Create an async context that doesn't cancel when server connection
825
        // closes.
826
        ctxAsync := context.Background()
17✔
827
        ctxAsync = log.WithContext(ctxAsync, l)
17✔
828
        ctxAsync = identity.WithContext(ctxAsync, idty)
17✔
829

17✔
830
        settings, _ := storage.SettingsFromContext(ctx)
17✔
831
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
17✔
832
        var artifactReader io.ReadCloser
17✔
833
        if skipVerify {
22✔
834
                artifactReader, err = d.objectStorage.GetObject(
5✔
835
                        ctxAsync,
5✔
836
                        model.ImagePathFromContext(ctx, intentID),
5✔
837
                )
5✔
838
        } else {
17✔
839
                artifactReader, err = d.objectStorage.GetObject(
12✔
840
                        ctxAsync,
12✔
841
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
12✔
842
                )
12✔
843
        }
12✔
844
        if err != nil {
21✔
845
                if errors.Is(err, storage.ErrObjectNotFound) {
6✔
846
                        return ErrUploadNotFound
2✔
847
                }
2✔
848
                return err
2✔
849
        }
850

851
        err = d.db.UpdateUploadIntentStatus(
13✔
852
                ctx,
13✔
853
                intentID,
13✔
854
                model.LinkStatusPending,
13✔
855
                model.LinkStatusProcessing,
13✔
856
        )
13✔
857
        if err != nil {
17✔
858
                errClose := artifactReader.Close()
4✔
859
                if errClose != nil {
6✔
860
                        l.Warnf("failed to close artifact reader: %s", errClose)
2✔
861
                }
2✔
862
                if errors.Is(err, store.ErrNotFound) {
6✔
863
                        return ErrUploadNotFound
2✔
864
                }
2✔
865
                return err
2✔
866
        }
867
        go d.processUploadedArtifact( // nolint:errcheck
9✔
868
                ctxAsync, intentID, artifactReader, skipVerify,
9✔
869
        )
9✔
870
        return nil
9✔
871
}
872

873
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
874
        return &model.ArtifactInfo{
1✔
875
                Format:  info.Format,
1✔
876
                Version: uint(info.Version),
1✔
877
        }
1✔
878
}
1✔
879

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

893
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
9✔
894
        metaArtifact := model.NewArtifactMeta()
9✔
895

9✔
896
        aReader := areader.NewReader(*r)
9✔
897

9✔
898
        // There is no signature verification here.
9✔
899
        // It is just simple check if artifact is signed or not.
9✔
900
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
9✔
901
                metaArtifact.Signed = true
×
902
                return nil
×
903
        }
×
904

905
        var err error
9✔
906
        if skipVerify {
14✔
907
                err = aReader.ReadArtifactHeaders()
5✔
908
                if err != nil {
9✔
909
                        return nil, errors.Wrap(err, "reading artifact error")
4✔
910
                }
4✔
911
        } else {
5✔
912
                err = aReader.ReadArtifact()
5✔
913
                if err != nil {
10✔
914
                        return nil, errors.Wrap(err, "reading artifact error")
5✔
915
                }
5✔
916
        }
917

918
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
919
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
920

1✔
921
        metaArtifact.Name = aReader.GetArtifactName()
1✔
922
        if metaArtifact.Info.Version == 3 {
2✔
923
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
924
                if err != nil {
1✔
925
                        return nil, errors.Wrap(err,
×
926
                                "error parsing version 3 artifact")
×
927
                }
×
928

929
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
930
                if err != nil {
1✔
931
                        return nil, errors.Wrap(err,
×
932
                                "error parsing version 3 artifact")
×
933
                }
×
934

935
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
936
        }
937

938
        for _, p := range aReader.GetHandlers() {
2✔
939
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
940
                if err != nil {
1✔
941
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
942
                }
×
943

944
                uMetadata, err := p.GetUpdateMetaData()
1✔
945
                if err != nil {
1✔
946
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
947
                }
×
948

949
                metaArtifact.Updates = append(
1✔
950
                        metaArtifact.Updates,
1✔
951
                        model.Update{
1✔
952
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
953
                                        Type: p.GetUpdateType(),
1✔
954
                                },
1✔
955
                                Files:    uFiles,
1✔
956
                                MetaData: uMetadata,
1✔
957
                        })
1✔
958
        }
959

960
        return metaArtifact, nil
1✔
961
}
962

963
func getArtifactIDs(artifacts []*model.Image) []string {
13✔
964
        artifactIDs := make([]string, 0, len(artifacts))
13✔
965
        for _, artifact := range artifacts {
26✔
966
                artifactIDs = append(artifactIDs, artifact.Id)
13✔
967
        }
13✔
968
        return artifactIDs
13✔
969
}
970

971
// deployments
972
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
8✔
973
        ids := make([]string, len(devices))
8✔
974
        for i, d := range devices {
16✔
975
                ids[i] = d.ID
8✔
976
        }
8✔
977

978
        return ids
8✔
979
}
980

981
// updateDeploymentConstructor fills devices list with device ids
982
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
983
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
10✔
984
        l := log.FromContext(ctx)
10✔
985

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

1014
        for {
22✔
1015
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
12✔
1016
                if err != nil {
14✔
1017
                        l.Errorf("error searching for devices")
2✔
1018
                        return nil, ErrModelInternal
2✔
1019
                }
2✔
1020
                if count < 1 {
12✔
1021
                        l.Errorf("no devices found")
2✔
1022
                        return nil, ErrNoDevices
2✔
1023
                }
2✔
1024
                if len(devices) < 1 {
8✔
1025
                        break
×
1026
                }
1027
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
8✔
1028
                if len(constructor.Devices) == count {
14✔
1029
                        break
6✔
1030
                }
1031
                searchParams.Page++
2✔
1032
        }
1033

1034
        return constructor, nil
6✔
1035
}
1036

1037
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1038
func (d *Deployments) CreateDeviceConfigurationDeployment(
1039
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1040
        deviceID, deploymentID string) (string, error) {
9✔
1041

9✔
1042
        if constructor == nil {
11✔
1043
                return "", ErrModelMissingInput
2✔
1044
        }
2✔
1045

1046
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
7✔
1047
                constructor,
7✔
1048
                deploymentID,
7✔
1049
        )
7✔
1050
        if err != nil {
7✔
1051
                return "", errors.Wrap(err, "failed to create deployment")
×
1052
        }
×
1053

1054
        deployment.DeviceList = []string{deviceID}
7✔
1055
        deployment.MaxDevices = 1
7✔
1056
        deployment.Configuration = []byte(constructor.Configuration)
7✔
1057
        deployment.Type = model.DeploymentTypeConfiguration
7✔
1058

7✔
1059
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
7✔
1060
        if err != nil {
9✔
1061
                return "", err
2✔
1062
        }
2✔
1063
        deployment.Groups = groups
5✔
1064

5✔
1065
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
8✔
1066
                if strings.Contains(err.Error(), "duplicate key error") {
4✔
1067
                        return "", ErrDuplicateDeployment
1✔
1068
                }
1✔
1069
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
4✔
1070
                        return "", ErrInvalidDeploymentID
1✔
1071
                }
1✔
1072
                return "", errors.Wrap(err, "Storing deployment data")
2✔
1073
        }
1074

1075
        return deployment.Id, nil
3✔
1076
}
1077

1078
// CreateDeployment precomputes new deployment and schedules it for devices.
1079
func (d *Deployments) CreateDeployment(ctx context.Context,
1080
        constructor *model.DeploymentConstructor) (string, error) {
17✔
1081

17✔
1082
        var err error
17✔
1083

17✔
1084
        if constructor == nil {
19✔
1085
                return "", ErrModelMissingInput
2✔
1086
        }
2✔
1087

1088
        if err := constructor.Validate(); err != nil {
15✔
1089
                return "", errors.Wrap(err, "Validating deployment")
×
1090
        }
×
1091

1092
        if len(constructor.Group) > 0 || constructor.AllDevices {
25✔
1093
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
10✔
1094
                if err != nil {
14✔
1095
                        return "", err
4✔
1096
                }
4✔
1097
        }
1098

1099
        deployment, err := model.NewDeploymentFromConstructor(constructor)
11✔
1100
        if err != nil {
11✔
1101
                return "", errors.Wrap(err, "failed to create deployment")
×
1102
        }
×
1103

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

1112
        if len(artifacts) == 0 {
12✔
1113
                return "", ErrNoArtifact
1✔
1114
        }
1✔
1115

1116
        deployment.Artifacts = getArtifactIDs(artifacts)
11✔
1117
        deployment.DeviceList = constructor.Devices
11✔
1118
        deployment.MaxDevices = len(constructor.Devices)
11✔
1119
        deployment.Type = model.DeploymentTypeSoftware
11✔
1120
        if len(constructor.Group) > 0 {
17✔
1121
                deployment.Groups = []string{constructor.Group}
6✔
1122
        }
6✔
1123

1124
        // single device deployment case
1125
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
16✔
1126
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
5✔
1127
                if err != nil {
5✔
1128
                        return "", err
×
1129
                }
×
1130
                deployment.Groups = groups
5✔
1131
        }
1132

1133
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
13✔
1134
                return "", errors.Wrap(err, "Storing deployment data")
2✔
1135
        }
2✔
1136

1137
        return deployment.Id, nil
9✔
1138
}
1139

1140
func (d *Deployments) getDeploymentGroups(
1141
        ctx context.Context,
1142
        devices []string,
1143
) ([]string, error) {
11✔
1144
        id := identity.FromContext(ctx)
11✔
1145

11✔
1146
        //only for single device deployment case
11✔
1147
        if len(devices) != 1 {
11✔
1148
                return nil, nil
×
1149
        }
×
1150

1151
        if id == nil {
12✔
1152
                id = &identity.Identity{}
1✔
1153
        }
1✔
1154

1155
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
11✔
1156
        if err != nil && err != inventory.ErrDevNotFound {
13✔
1157
                return nil, err
2✔
1158
        }
2✔
1159
        return groups, nil
9✔
1160
}
1161

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

1175
        return false, nil
1✔
1176
}
1177

1178
// GetDeployment fetches deployment by ID
1179
func (d *Deployments) GetDeployment(ctx context.Context,
1180
        deploymentID string) (*model.Deployment, error) {
1✔
1181

1✔
1182
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1183
        if err != nil {
1✔
1184
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1185
        }
×
1186

1187
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1188
                return nil, err
×
1189
        }
×
1190

1191
        return deployment, nil
1✔
1192
}
1193

1194
// ImageUsedInActiveDeployment checks if specified image is in use by deployments Image is
1195
// considered to be in use if it's participating in at lest one non success/error deployment.
1196
func (d *Deployments) ImageUsedInActiveDeployment(ctx context.Context,
1197
        imageID string) (bool, error) {
7✔
1198

7✔
1199
        var found bool
7✔
1200

7✔
1201
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
7✔
1202
        if err != nil {
9✔
1203
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
2✔
1204
        }
2✔
1205

1206
        if found {
6✔
1207
                return found, nil
1✔
1208
        }
1✔
1209

1210
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
5✔
1211
                imageID, model.ActiveDeploymentStatuses()...)
5✔
1212
        if err != nil {
7✔
1213
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
2✔
1214
        }
2✔
1215

1216
        return found, nil
3✔
1217
}
1218

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

×
1223
        var found bool
×
1224

×
1225
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1226
        if err != nil {
×
1227
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1228
        }
×
1229

1230
        if found {
×
1231
                return found, nil
×
1232
        }
×
1233

1234
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
1235
        if err != nil {
×
1236
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
1237
        }
×
1238

1239
        return found, nil
×
1240
}
1241

1242
// Retrieves the model.Deployment and model.DeviceDeployment structures
1243
// for the device. Upon error, nil is returned for both deployment structures.
1244
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1245
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
3✔
1246

3✔
1247
        // Retrieve device deployment
3✔
1248
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
3✔
1249

3✔
1250
        if err != nil {
3✔
1251
                return nil, nil, errors.Wrap(err,
×
1252
                        "Searching for oldest active deployment for the device")
×
1253
        } else if deviceDeployment == nil {
4✔
1254
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1255
        }
1✔
1256

1257
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
3✔
1258
        if err != nil {
3✔
1259
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1260
        }
×
1261
        if deployment == nil {
3✔
1262
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1263
        }
×
1264

1265
        return deployment, deviceDeployment, nil
3✔
1266
}
1267

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

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

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

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

1318
        return nil, nil, nil
×
1319
}
1320

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

1333
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
11✔
1334
        deviceDeployment.Status = status
11✔
1335
        deviceDeployment.Active = status.Active()
11✔
1336
        deviceDeployment.Created = deployment.Created
11✔
1337

11✔
1338
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
11✔
1339
                return nil, err
×
1340
        }
×
1341

1342
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
11✔
1343
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
11✔
1344
                return nil, err
×
1345
        }
×
1346

1347
        // after inserting new device deployment update deployment stats
1348
        // in the database and locally, and update deployment status
1349
        if err := d.db.UpdateStatsInc(
11✔
1350
                ctx, deployment.Id,
11✔
1351
                prevStatus, status,
11✔
1352
        ); err != nil {
11✔
1353
                return nil, err
×
1354
        }
×
1355

1356
        deployment.Stats.Inc(status)
11✔
1357

11✔
1358
        err = d.recalcDeploymentStatus(ctx, deployment)
11✔
1359
        if err != nil {
11✔
1360
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1361
        }
×
1362

1363
        if !status.Active() {
21✔
1364
                err := d.reindexDevice(ctx, deviceID)
10✔
1365
                if err != nil {
10✔
1366
                        l := log.FromContext(ctx)
×
1367
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1368
                }
×
1369
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
10✔
1370
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
10✔
1371
                        l := log.FromContext(ctx)
×
1372
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1373
                }
×
1374
        }
1375

1376
        return deviceDeployment, nil
11✔
1377
}
1378

1379
func (d *Deployments) isDevicePartOfDeployment(
1380
        ctx context.Context,
1381
        deviceID string,
1382
        deployment *model.Deployment,
1383
) (bool, error) {
15✔
1384
        for _, id := range deployment.DeviceList {
26✔
1385
                if id == deviceID {
22✔
1386
                        return true, nil
11✔
1387
                }
11✔
1388
        }
1389
        return false, nil
5✔
1390
}
1391

1392
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1393
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1394
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
3✔
1395

3✔
1396
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
3✔
1397
        if err != nil {
3✔
1398
                return nil, ErrModelInternal
×
1399
        } else if deployment == nil {
4✔
1400
                return nil, nil
1✔
1401
        }
1✔
1402

1403
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
3✔
1404
        if err != nil {
4✔
1405
                return nil, err
1✔
1406
        }
1✔
1407
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
3✔
1408
}
1409

1410
func (d *Deployments) getDeploymentInstructions(
1411
        ctx context.Context,
1412
        deployment *model.Deployment,
1413
        deviceDeployment *model.DeviceDeployment,
1414
        request *model.DeploymentNextRequest,
1415
) (*model.DeploymentInstructions, error) {
3✔
1416

3✔
1417
        var newArtifactAssigned bool
3✔
1418

3✔
1419
        l := log.FromContext(ctx)
3✔
1420

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

1437
        // assing artifact to the device deployment
1438
        // only if it was not assgined previously
1439
        if deviceDeployment.Image == nil {
6✔
1440
                if err := d.assignArtifact(
3✔
1441
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
3✔
1442
                        return nil, err
×
1443
                }
×
1444
                newArtifactAssigned = true
3✔
1445
        }
1446

1447
        if deviceDeployment.Image == nil {
3✔
1448
                // No artifact - return empty response
×
1449
                return nil, nil
×
1450
        }
×
1451

1452
        // if the deployment is not forcing the installation, and
1453
        // if artifact was recognized as already installed, and this is
1454
        // a new device deployment - indicated by device deployment status "pending",
1455
        // handle already installed artifact case
1456
        if !deployment.ForceInstallation &&
3✔
1457
                d.isAlreadyInstalled(request, deviceDeployment) &&
3✔
1458
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
6✔
1459
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
3✔
1460
        }
3✔
1461

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

1472
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1473
        if err != nil {
1✔
1474
                return nil, err
×
1475
        }
×
1476

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

1488
        instructions := &model.DeploymentInstructions{
1✔
1489
                ID: deviceDeployment.DeploymentId,
1✔
1490
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1491
                        ID: deviceDeployment.Image.Id,
1✔
1492
                        ArtifactName: deviceDeployment.Image.
1✔
1493
                                ArtifactMeta.Name,
1✔
1494
                        Source: *link,
1✔
1495
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1496
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1497
                },
1✔
1498
        }
1✔
1499

1✔
1500
        return instructions, nil
1✔
1501
}
1502

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

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

1542
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1543
// ID `deviceID`. Returns nil if update was successful.
1544
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1545
        deviceID string, ddState model.DeviceDeploymentState) error {
11✔
1546

11✔
1547
        l := log.FromContext(ctx)
11✔
1548

11✔
1549
        l.Infof("New status: %s for device %s deployment: %v", ddState.Status, deviceID, deploymentID)
11✔
1550

11✔
1551
        var finishTime *time.Time = nil
11✔
1552
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
18✔
1553
                now := time.Now()
7✔
1554
                finishTime = &now
7✔
1555
        }
7✔
1556

1557
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
11✔
1558
        if err == mongo.ErrStorageNotFound {
13✔
1559
                return ErrStorageNotFound
2✔
1560
        } else if err != nil {
11✔
1561
                return err
×
1562
        }
×
1563

1564
        currentStatus := dd.Status
9✔
1565

9✔
1566
        if currentStatus == model.DeviceDeploymentStatusAborted {
9✔
1567
                return ErrDeploymentAborted
×
1568
        }
×
1569

1570
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
9✔
1571
                return ErrDeviceDecommissioned
×
1572
        }
×
1573

1574
        // nothing to do
1575
        if ddState.Status == currentStatus {
9✔
1576
                return nil
×
1577
        }
×
1578

1579
        // update finish time
1580
        ddState.FinishTime = finishTime
9✔
1581

9✔
1582
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
9✔
1583
                deviceID, deploymentID, ddState)
9✔
1584
        if err != nil {
9✔
1585
                return err
×
1586
        }
×
1587

1588
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
9✔
1589
                return err
×
1590
        }
×
1591

1592
        // fetch deployment stats and update deployment status
1593
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
9✔
1594
        if err != nil {
9✔
1595
                return errors.Wrap(err, "failed when searching for deployment")
×
1596
        }
×
1597

1598
        err = d.recalcDeploymentStatus(ctx, deployment)
9✔
1599
        if err != nil {
9✔
1600
                return errors.Wrap(err, "failed to update deployment status")
×
1601
        }
×
1602

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

1622
        return nil
9✔
1623
}
1624

1625
// recalcDeploymentStatus inspects the deployment stats and
1626
// recalculates and updates its status
1627
// it should be used whenever deployment stats are touched
1628
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
19✔
1629
        status := dep.GetStatus()
19✔
1630

19✔
1631
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
19✔
1632
                return err
×
1633
        }
×
1634

1635
        return nil
19✔
1636
}
1637

1638
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1639
        deploymentID string) (model.Stats, error) {
1✔
1640

1✔
1641
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1642

1✔
1643
        if err != nil {
1✔
1644
                return nil, errors.Wrap(err, "checking deployment id")
×
1645
        }
×
1646

1647
        if deployment == nil {
1✔
1648
                return nil, nil
×
1649
        }
×
1650

1651
        return deployment.Stats, nil
1✔
1652
}
1653
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1654
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1655

×
1656
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1657

×
1658
        if err != nil {
×
1659
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1660
        }
×
1661

1662
        if deploymentStats == nil {
×
1663
                return nil, ErrModelDeploymentNotFound
×
1664
        }
×
1665

1666
        return deploymentStats, nil
×
1667
}
1668

1669
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1670
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1671
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1672

1✔
1673
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1674
        if err != nil {
1✔
1675
                return nil, ErrModelInternal
×
1676
        }
×
1677

1678
        if deployment == nil {
1✔
1679
                return nil, ErrModelDeploymentNotFound
×
1680
        }
×
1681

1682
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1683
        if err != nil {
1✔
1684
                return nil, ErrModelInternal
×
1685
        }
×
1686

1687
        return statuses, nil
1✔
1688
}
1689

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

1✔
1693
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1694
        if err != nil {
1✔
1695
                return nil, -1, ErrModelInternal
×
1696
        }
×
1697

1698
        if deployment == nil {
1✔
1699
                return nil, -1, ErrModelDeploymentNotFound
×
1700
        }
×
1701

1702
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1703
        if err != nil {
1✔
1704
                return nil, -1, ErrModelInternal
×
1705
        }
×
1706

1707
        return statuses, totalCount, nil
1✔
1708
}
1709

1710
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1711
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
8✔
1712
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
8✔
1713
        if err != nil {
10✔
1714
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
2✔
1715
        }
2✔
1716

1717
        deploymentIDs := make([]string, len(deviceDeployments))
6✔
1718
        for i, deviceDeployment := range deviceDeployments {
18✔
1719
                deploymentIDs[i] = deviceDeployment.DeploymentId
12✔
1720
        }
12✔
1721

1722
        deployments, _, err := d.db.Find(ctx, model.Query{
6✔
1723
                IDs:          deploymentIDs,
6✔
1724
                Limit:        len(deviceDeployments),
6✔
1725
                DisableCount: true,
6✔
1726
        })
6✔
1727
        if err != nil {
8✔
1728
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
2✔
1729
        }
2✔
1730

1731
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
4✔
1732
        for _, deployment := range deployments {
10✔
1733
                deploymentsMap[deployment.Id] = deployment
6✔
1734
        }
6✔
1735

1736
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
4✔
1737
        for i, deviceDeployment := range deviceDeployments {
12✔
1738
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
14✔
1739
                        res = append(res, model.DeviceDeploymentListItem{
6✔
1740
                                Id:         deviceDeployment.Id,
6✔
1741
                                Deployment: deployment,
6✔
1742
                                Device:     &deviceDeployments[i],
6✔
1743
                        })
6✔
1744
                } else {
8✔
1745
                        res = append(res, model.DeviceDeploymentListItem{
2✔
1746
                                Id:     deviceDeployment.Id,
2✔
1747
                                Device: &deviceDeployments[i],
2✔
1748
                        })
2✔
1749
                }
2✔
1750
        }
1751

1752
        return res, totalCount, nil
4✔
1753
}
1754

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

1771
        return nil
11✔
1772
}
1773

1774
func (d *Deployments) LookupDeployment(ctx context.Context,
1775
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1776
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1777

1✔
1778
        if err != nil {
1✔
1779
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1780
        }
×
1781

1782
        if list == nil {
2✔
1783
                return make([]*model.Deployment, 0), 0, nil
1✔
1784
        }
1✔
1785

1786
        for _, deployment := range list {
×
1787
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1788
                        return nil, 0, err
×
1789
                }
×
1790
        }
1791

1792
        return list, totalCount, nil
×
1793
}
1794

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

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

1810
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1811
                if err != nil {
×
1812
                        return err
×
1813
                } else {
×
1814
                        return ErrModelDeploymentNotFound
×
1815
                }
×
1816
        }
1817

1818
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1819
                return err
×
1820
        }
×
1821

1822
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1823
                deviceID, deploymentID, true)
1✔
1824
}
1825

1826
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1827
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1828

1✔
1829
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1830
                deviceID, deploymentID)
1✔
1831
}
1✔
1832

1833
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1834
        deploymentID string, deviceID string) (bool, error) {
1✔
1835
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1836
}
1✔
1837

1838
// AbortDeployment aborts deployment for devices and updates deployment stats
1839
func (d *Deployments) AbortDeployment(ctx context.Context, deploymentID string) error {
9✔
1840

9✔
1841
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
11✔
1842
                return err
2✔
1843
        }
2✔
1844

1845
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
7✔
1846
                ctx, deploymentID)
7✔
1847
        if err != nil {
9✔
1848
                return err
2✔
1849
        }
2✔
1850

1851
        // update statistics
1852
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
7✔
1853
                return errors.Wrap(err, "failed to update deployment stats")
2✔
1854
        }
2✔
1855

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

1866
        return nil
3✔
1867
}
1868

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

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

1935
        if err := d.reindexDevice(ctx, deviceId); err != nil {
26✔
1936
                l := log.FromContext(ctx)
×
1937
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1938
        }
×
1939

1940
        return nil
26✔
1941
}
1942

1943
// DecommissionDevice updates the status of all the pending and active deployments for a device
1944
// to decommissioned
1945
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
14✔
1946
        return d.updateDeviceDeploymentsStatus(
14✔
1947
                ctx,
14✔
1948
                deviceId,
14✔
1949
                model.DeviceDeploymentStatusDecommissioned,
14✔
1950
        )
14✔
1951
}
14✔
1952

1953
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1954
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
16✔
1955
        return d.updateDeviceDeploymentsStatus(
16✔
1956
                ctx,
16✔
1957
                deviceId,
16✔
1958
                model.DeviceDeploymentStatusAborted,
16✔
1959
        )
16✔
1960
}
16✔
1961

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

1971
        // no device deployments to update
1972
        if len(dd) <= 0 {
4✔
1973
                return nil
×
1974
        }
×
1975

1976
        // mark device deployments as deleted
1977
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
6✔
1978
                return err
2✔
1979
        }
2✔
1980

1981
        // trigger reindexing of updated device deployments
1982
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
2✔
1983
        for i, d := range dd {
4✔
1984
                deviceDeployments[i].ID = d.Id
2✔
1985
                deviceDeployments[i].DeviceID = d.DeviceId
2✔
1986
                deviceDeployments[i].DeploymentID = d.DeploymentId
2✔
1987
        }
2✔
1988
        return d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
2✔
1989
}
1990

1991
// Storage settings
1992
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
5✔
1993
        settings, err := d.db.GetStorageSettings(ctx)
5✔
1994
        if err != nil {
7✔
1995
                return nil, errors.Wrap(err, "Searching for settings failed")
2✔
1996
        }
2✔
1997

1998
        return settings, nil
3✔
1999
}
2000

2001
func (d *Deployments) SetStorageSettings(
2002
        ctx context.Context,
2003
        storageSettings *model.StorageSettings,
2004
) error {
7✔
2005
        if storageSettings != nil {
14✔
2006
                ctx = storage.SettingsWithContext(ctx, storageSettings)
7✔
2007
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
7✔
2008
                        return errors.WithMessage(err,
×
2009
                                "the provided storage settings failed the health check",
×
2010
                        )
×
2011
                }
×
2012
        }
2013
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
11✔
2014
                return errors.Wrap(err, "Failed to save settings")
4✔
2015
        }
4✔
2016

2017
        return nil
3✔
2018
}
2019

2020
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
15✔
2021
        d.reportingClient = c
15✔
2022
        return d
15✔
2023
}
15✔
2024

2025
func (d *Deployments) haveReporting() bool {
12✔
2026
        return d.reportingClient != nil
12✔
2027
}
12✔
2028

2029
func (d *Deployments) search(
2030
        ctx context.Context,
2031
        tid string,
2032
        parms model.SearchParams,
2033
) ([]model.InvDevice, int, error) {
12✔
2034
        if d.haveReporting() {
14✔
2035
                return d.reportingClient.Search(ctx, tid, parms)
2✔
2036
        } else {
12✔
2037
                return d.inventoryClient.Search(ctx, tid, parms)
10✔
2038
        }
10✔
2039
}
2040

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

2054
        // Assign artifacts to the deployments with given artifact name
2055
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
2✔
2056
        if err != nil {
2✔
2057
                return errors.Wrap(err, "Finding artifact with given name")
×
2058
        }
×
2059

2060
        if len(artifacts) == 0 {
2✔
2061
                return ErrNoArtifact
×
2062
        }
×
2063
        artifactIDs := getArtifactIDs(artifacts)
2✔
2064
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
2✔
2065
}
2066

2067
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
49✔
2068
        if d.reportingClient != nil {
54✔
2069
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
5✔
2070
        }
5✔
2071
        return nil
44✔
2072
}
2073

2074
func (d *Deployments) reindexDeployment(ctx context.Context,
2075
        deviceID, deploymentID, ID string) error {
33✔
2076
        if d.reportingClient != nil {
38✔
2077
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
5✔
2078
        }
5✔
2079
        return nil
28✔
2080
}
2081

2082
func (d *Deployments) updateReleaseEditArtifact(
2083
        ctx context.Context,
2084
        artifactToEdit *model.Image,
NEW
2085
) error {
×
NEW
2086

×
NEW
2087
        if artifactToEdit == nil {
×
NEW
2088
                return ErrEmptyArtifact
×
NEW
2089
        }
×
NEW
2090
        return d.db.UpdateReleaseArtifactDescription(
×
NEW
2091
                ctx,
×
NEW
2092
                artifactToEdit,
×
NEW
2093
                artifactToEdit.ArtifactMeta.Name,
×
NEW
2094
        )
×
2095
}
2096

2097
func (d *Deployments) updateRelease(
2098
        ctx context.Context,
2099
        artifactToAdd *model.Image,
2100
        artifactToRemove *model.Image,
2101
) error {
1✔
2102
        name := ""
1✔
2103
        if artifactToRemove != nil {
2✔
2104
                name = artifactToRemove.ArtifactMeta.Name
1✔
2105
        } else if artifactToAdd != nil {
3✔
2106
                name = artifactToAdd.ArtifactMeta.Name
1✔
2107
        } else {
1✔
NEW
2108
                return ErrEmptyArtifact
×
NEW
2109
        }
×
2110

2111
        return d.db.UpdateReleaseArtifacts(ctx, artifactToAdd, artifactToRemove, name)
1✔
2112
}
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