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

mendersoftware / deployments / 1022633108

02 Oct 2023 08:56AM UTC coverage: 80.458% (-0.03%) from 80.483%
1022633108

Pull #931

gitlab-ci

merlin-northern
feat: direct upload with skip verify: set metadata on complete

Changelog: Title
Ticket: MEN-6696
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #931: feat: direct upload with skip verify: set metadata on complete

21 of 76 new or added lines in 4 files covered. (27.63%)

84 existing lines in 1 file now uncovered.

7835 of 9738 relevant lines covered (80.46%)

34.99 hits per line

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

76.24
/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(
131
                ctx context.Context,
132
                intentID string,
133
                skipVerify bool,
134
                metadata *model.DirectUploadMetadata,
135
        ) error
136
        GetImage(ctx context.Context, id string) (*model.Image, error)
137
        DeleteImage(ctx context.Context, imageID string) error
138
        CreateImage(ctx context.Context,
139
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
140
        GenerateImage(ctx context.Context,
141
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
142
        GenerateConfigurationImage(
143
                ctx context.Context,
144
                deviceType string,
145
                deploymentID string,
146
        ) (io.Reader, error)
147
        EditImage(ctx context.Context, id string,
148
                constructorData *model.ImageMeta) (bool, error)
149

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

195
        // releases
196
        ReplaceReleaseTags(ctx context.Context, releaseName string, tags model.Tags) error
197
        UpdateRelease(ctx context.Context, releaseName string, release model.ReleasePatch) error
198
        ListReleaseTags(ctx context.Context) (model.Tags, error)
199
        GetReleasesUpdateTypes(ctx context.Context) ([]string, error)
200
}
201

202
type Deployments struct {
203
        db              store.DataStore
204
        objectStorage   storage.ObjectStorage
205
        workflowsClient workflows.Client
206
        inventoryClient inventory.Client
207
        reportingClient reporting.Client
208
}
209

210
// Compile-time check
211
var _ App = &Deployments{}
212

213
func NewDeployments(
214
        storage store.DataStore,
215
        objectStorage storage.ObjectStorage,
216
        maxActiveDeployments int64,
217
        withAuditLogs bool,
218
) *Deployments {
74✔
219
        return &Deployments{
74✔
220
                db:              storage,
74✔
221
                objectStorage:   objectStorage,
74✔
222
                workflowsClient: workflows.NewClient(),
74✔
223
                inventoryClient: inventory.NewClient(),
74✔
224
        }
74✔
225
}
74✔
226

227
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
4✔
228
        d.workflowsClient = workflowsClient
4✔
229
}
4✔
230

231
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
8✔
232
        d.inventoryClient = inventoryClient
8✔
233
}
8✔
234

235
func (d *Deployments) HealthCheck(ctx context.Context) error {
6✔
236
        err := d.db.Ping(ctx)
6✔
237
        if err != nil {
7✔
238
                return errors.Wrap(err, "error reaching MongoDB")
1✔
239
        }
1✔
240
        err = d.objectStorage.HealthCheck(ctx)
5✔
241
        if err != nil {
6✔
242
                return errors.Wrap(
1✔
243
                        err,
1✔
244
                        "error reaching artifact storage service",
1✔
245
                )
1✔
246
        }
1✔
247

248
        err = d.workflowsClient.CheckHealth(ctx)
4✔
249
        if err != nil {
5✔
250
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
251
        }
1✔
252

253
        err = d.inventoryClient.CheckHealth(ctx)
3✔
254
        if err != nil {
4✔
255
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
256
        }
1✔
257

258
        if d.reportingClient != nil {
4✔
259
                err = d.reportingClient.CheckHealth(ctx)
2✔
260
                if err != nil {
3✔
261
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
262
                }
1✔
263
        }
264
        return nil
1✔
265
}
266

267
func (d *Deployments) contextWithStorageSettings(
268
        ctx context.Context,
269
) (context.Context, error) {
26✔
270
        var err error
26✔
271
        settings, ok := storage.SettingsFromContext(ctx)
26✔
272
        if !ok {
48✔
273
                settings, err = d.db.GetStorageSettings(ctx)
22✔
274
        }
22✔
275
        if err != nil {
28✔
276
                return nil, err
2✔
277
        } else if settings != nil {
26✔
278
                err = settings.Validate()
×
279
                if err != nil {
×
280
                        return nil, err
×
281
                }
×
282
        }
283
        return storage.SettingsWithContext(ctx, settings), nil
24✔
284
}
285

286
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
3✔
287
        limit, err := d.db.GetLimit(ctx, name)
3✔
288
        if err == mongo.ErrLimitNotFound {
4✔
289
                return &model.Limit{
1✔
290
                        Name:  name,
1✔
291
                        Value: 0,
1✔
292
                }, nil
1✔
293

1✔
294
        } else if err != nil {
4✔
295
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
296
        }
1✔
297
        return limit, nil
1✔
298
}
299

300
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
3✔
301
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
4✔
302
                return errors.Wrap(err, "failed to provision tenant")
1✔
303
        }
1✔
304

305
        return nil
2✔
306
}
307

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

316
func (d *Deployments) saveUpdateTypes(ctx context.Context, image *model.Image) {
1✔
317
        l := log.FromContext(ctx)
1✔
318
        if image != nil && image.ArtifactMeta != nil && len(image.ArtifactMeta.Updates) > 0 {
2✔
319
                i := 0
1✔
320
                updateTypes := make([]string, len(image.ArtifactMeta.Updates))
1✔
321
                for _, t := range image.ArtifactMeta.Updates {
2✔
322
                        if t.TypeInfo.Type == nil {
2✔
323
                                continue
1✔
324
                        }
325
                        updateTypes[i] = *t.TypeInfo.Type
1✔
326
                        i++
1✔
327
                }
328
                err := d.db.SaveUpdateTypes(ctx, updateTypes[:i])
1✔
329
                if err != nil {
1✔
330
                        l.Errorf(
×
331
                                "error while saving the update types for the artifact: %s",
×
332
                                err.Error(),
×
333
                        )
×
334
                }
×
335
        }
336
}
337

338
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
339
// and creates image structure in the system.
340
// Returns image ID, artifact file ID and nil on success.
341
func (d *Deployments) handleArtifact(ctx context.Context,
342
        multipartUploadMsg *model.MultipartUploadMsg,
343
        skipVerify bool,
344
        metadata *model.DirectUploadMetadata,
345
) (string, error) {
5✔
346

5✔
347
        l := log.FromContext(ctx)
5✔
348
        ctx, err := d.contextWithStorageSettings(ctx)
5✔
349
        if err != nil {
5✔
350
                return "", err
×
351
        }
×
352

353
        // create pipe
354
        pR, pW := io.Pipe()
5✔
355

5✔
356
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
5✔
357

5✔
358
        tee := io.TeeReader(artifactReader, pW)
5✔
359

5✔
360
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
5✔
361
        if err != nil {
6✔
362
                uid, _ = uuid.NewRandom()
1✔
363
        }
1✔
364
        artifactID := uid.String()
5✔
365

5✔
366
        ch := make(chan error)
5✔
367
        // create goroutine for artifact upload
5✔
368
        //
5✔
369
        // reading from the pipe (which is done in UploadArtifact method) is a blocking operation
5✔
370
        // and cannot be done in the same goroutine as writing to the pipe
5✔
371
        //
5✔
372
        // uploading and parsing artifact in the same process will cause in a deadlock!
5✔
373
        //nolint:errcheck
5✔
374
        go func() (err error) {
10✔
375
                defer func() { ch <- err }()
10✔
376
                if skipVerify {
8✔
377
                        err = nil
3✔
378
                        io.Copy(io.Discard, pR)
3✔
379
                        return nil
3✔
380
                }
3✔
381
                err = d.objectStorage.PutObject(
3✔
382
                        ctx, model.ImagePathFromContext(ctx, artifactID), pR,
3✔
383
                )
3✔
384
                if err != nil {
4✔
385
                        pR.CloseWithError(err)
1✔
386
                }
1✔
387
                return err
3✔
388
        }()
389

390
        // parse artifact
391
        // artifact library reads all the data from the given reader
392
        metaArtifactConstructor, err := getMetaFromArchive(&tee, skipVerify)
5✔
393
        if err != nil {
10✔
394
                _ = pW.CloseWithError(err)
5✔
395
                <-ch
5✔
396
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
5✔
397
        }
5✔
398
        validMetadata := false
1✔
399
        if skipVerify && metadata != nil {
1✔
NEW
400
                // this means we got files and metadata separately
×
NEW
401
                // we can now put it in the metaArtifactConstructor
×
NEW
402
                // after validating that the files information match the artifact
×
NEW
403
                validMetadata = validUpdates(metaArtifactConstructor.Updates, metadata.Updates)
×
NEW
404
                if validMetadata {
×
NEW
405
                        metaArtifactConstructor.Updates = metadata.Updates
×
NEW
406
                }
×
407
        }
408
        // validate artifact metadata
409
        if err = metaArtifactConstructor.Validate(); err != nil {
1✔
410
                return artifactID, ErrModelInvalidMetadata
×
411
        }
×
412

413
        if !skipVerify {
2✔
414
                // read the rest of the data,
1✔
415
                // just in case the artifact library did not read all the data from the reader
1✔
416
                _, err = io.Copy(io.Discard, tee)
1✔
417
                if err != nil {
1✔
418
                        // CloseWithError will cause the reading end to abort upload.
×
419
                        _ = pW.CloseWithError(err)
×
420
                        <-ch
×
421
                        return artifactID, err
×
422
                }
×
423
        }
424

425
        // close the pipe
426
        pW.Close()
1✔
427

1✔
428
        // collect output from the goroutine
1✔
429
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
430
                return artifactID, uploadResponseErr
×
431
        }
×
432

433
        size := artifactReader.Count()
1✔
434
        if skipVerify && validMetadata {
1✔
NEW
435
                size = metadata.Size
×
NEW
436
        }
×
437
        image := model.NewImage(
1✔
438
                artifactID,
1✔
439
                multipartUploadMsg.MetaConstructor,
1✔
440
                metaArtifactConstructor,
1✔
441
                size,
1✔
442
        )
1✔
443

1✔
444
        // save image structure in the system
1✔
445
        if err = d.db.InsertImage(ctx, image); err != nil {
1✔
446
                // Try to remove the storage from s3.
×
447
                if errDelete := d.objectStorage.DeleteObject(
×
448
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
449
                ); errDelete != nil {
×
450
                        l.Errorf(
×
451
                                "failed to clean up artifact storage after failure: %s",
×
452
                                errDelete,
×
453
                        )
×
454
                }
×
455
                if idxErr, ok := err.(*model.ConflictError); ok {
×
456
                        return artifactID, idxErr
×
457
                }
×
458
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
459
        }
460
        d.saveUpdateTypes(ctx, image)
1✔
461

1✔
462
        // update release
1✔
463
        if err := d.updateRelease(ctx, image, nil); err != nil {
1✔
464
                return "", err
×
465
        }
×
466

467
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
468
                return "", errors.Wrap(err, "fail to update deployments")
×
469
        }
×
470

471
        return artifactID, nil
1✔
472
}
473

NEW
474
func validUpdates(constructorUpdates []model.Update, metadataUpdates []model.Update) bool {
×
NEW
475
        valid := false
×
NEW
476
        if len(constructorUpdates) == len(metadataUpdates) {
×
NEW
477
                valid = true
×
NEW
478
                for _, update := range constructorUpdates {
×
NEW
479
                        for _, updateExternal := range metadataUpdates {
×
NEW
480
                                if !update.Match(updateExternal) {
×
NEW
481
                                        valid = false
×
NEW
482
                                        break
×
483
                                }
484
                        }
485
                }
486
        }
NEW
487
        return valid
×
488
}
489

490
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
491
// creates image structure in the system, and starts the workflow to generate the
492
// artifact from them.
493
// Returns image ID and nil on success.
494
func (d *Deployments) GenerateImage(ctx context.Context,
495
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
11✔
496

11✔
497
        if multipartGenerateImageMsg == nil {
12✔
498
                return "", ErrModelMultipartUploadMsgMalformed
1✔
499
        }
1✔
500

501
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
10✔
502
        if err != nil {
15✔
503
                return "", err
5✔
504
        }
5✔
505
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
6✔
506
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
507
        }
1✔
508
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
5✔
509
        if err != nil {
7✔
510
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
3✔
511
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
512
                }
1✔
513
                return "", err
1✔
514
        }
515

516
        return multipartGenerateImageMsg.ArtifactID, err
3✔
517
}
518

519
func (d *Deployments) GenerateConfigurationImage(
520
        ctx context.Context,
521
        deviceType string,
522
        deploymentID string,
523
) (io.Reader, error) {
5✔
524
        var buf bytes.Buffer
5✔
525
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
526
        if err != nil {
6✔
527
                return nil, err
1✔
528
        } else if dpl == nil {
6✔
529
                return nil, ErrModelDeploymentNotFound
1✔
530
        }
1✔
531
        var metaData map[string]interface{}
3✔
532
        err = json.Unmarshal(dpl.Configuration, &metaData)
3✔
533
        if err != nil {
4✔
534
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
1✔
535
        }
1✔
536

537
        artieWriter := awriter.NewWriter(&buf, artifact.NewCompressorNone())
2✔
538
        module := handlers.NewModuleImage(ArtifactConfigureType)
2✔
539
        err = artieWriter.WriteArtifact(&awriter.WriteArtifactArgs{
2✔
540
                Format:  "mender",
2✔
541
                Version: 3,
2✔
542
                Devices: []string{deviceType},
2✔
543
                Name:    dpl.ArtifactName,
2✔
544
                Updates: &awriter.Updates{Updates: []handlers.Composer{module}},
2✔
545
                Depends: &artifact.ArtifactDepends{
2✔
546
                        CompatibleDevices: []string{deviceType},
2✔
547
                },
2✔
548
                Provides: &artifact.ArtifactProvides{
2✔
549
                        ArtifactName: dpl.ArtifactName,
2✔
550
                },
2✔
551
                MetaData: metaData,
2✔
552
                TypeInfoV3: &artifact.TypeInfoV3{
2✔
553
                        Type: &ArtifactConfigureType,
2✔
554
                        ArtifactProvides: artifact.TypeInfoProvides{
2✔
555
                                ArtifactConfigureProvides: dpl.ArtifactName,
2✔
556
                        },
2✔
557
                        ArtifactDepends:        artifact.TypeInfoDepends{},
2✔
558
                        ClearsArtifactProvides: []string{ArtifactConfigureProvidesCleared},
2✔
559
                },
2✔
560
        })
2✔
561

2✔
562
        return &buf, err
2✔
563
}
564

565
// handleRawFile parses raw data, uploads it to the file storage,
566
// and starts the workflow to generate the artifact.
567
// Returns the object path to the file and nil on success.
568
func (d *Deployments) handleRawFile(ctx context.Context,
569
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
10✔
570
        l := log.FromContext(ctx)
10✔
571
        uid, _ := uuid.NewRandom()
10✔
572
        artifactID := uid.String()
10✔
573
        multipartMsg.ArtifactID = artifactID
10✔
574
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
10✔
575

10✔
576
        // check if artifact is unique
10✔
577
        // artifact is considered to be unique if there is no artifact with the same name
10✔
578
        // and supporting the same platform in the system
10✔
579
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
10✔
580
                multipartMsg.Name,
10✔
581
                multipartMsg.DeviceTypesCompatible,
10✔
582
        )
10✔
583
        if err != nil {
11✔
584
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
1✔
585
        }
1✔
586
        if !isArtifactUnique {
10✔
587
                return "", ErrModelArtifactNotUnique
1✔
588
        }
1✔
589

590
        ctx, err = d.contextWithStorageSettings(ctx)
8✔
591
        if err != nil {
8✔
592
                return "", err
×
593
        }
×
594
        err = d.objectStorage.PutObject(
8✔
595
                ctx, filePath, multipartMsg.FileReader,
8✔
596
        )
8✔
597
        if err != nil {
9✔
598
                return "", err
1✔
599
        }
1✔
600
        defer func() {
14✔
601
                if err != nil {
9✔
602
                        e := d.objectStorage.DeleteObject(ctx, filePath)
2✔
603
                        if e != nil {
4✔
604
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
2✔
605
                                        filePath, e)
2✔
606
                        }
2✔
607
                }
608
        }()
609

610
        link, err := d.objectStorage.GetRequest(
7✔
611
                ctx,
7✔
612
                filePath,
7✔
613
                path.Base(filePath),
7✔
614
                DefaultImageGenerationLinkExpire,
7✔
615
        )
7✔
616
        if err != nil {
8✔
617
                return "", err
1✔
618
        }
1✔
619
        multipartMsg.GetArtifactURI = link.Uri
6✔
620

6✔
621
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
6✔
622
        if err != nil {
7✔
623
                return "", err
1✔
624
        }
1✔
625
        multipartMsg.DeleteArtifactURI = link.Uri
5✔
626

5✔
627
        return artifactID, nil
5✔
628
}
629

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

1✔
634
        image, err := d.db.FindImageByID(ctx, id)
1✔
635
        if err != nil {
1✔
636
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
637
        }
×
638

639
        if image == nil {
2✔
640
                return nil, nil
1✔
641
        }
1✔
642

643
        return image, nil
1✔
644
}
645

646
// DeleteImage removes metadata and image file
647
// Noop for not existing images
648
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
649
// file is needed
650
// In case of already finished updates only image file is not needed, metadata is attached directly
651
// to device deployment therefore we still have some information about image that have been used
652
// (but not the file)
653
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
1✔
654
        found, err := d.GetImage(ctx, imageID)
1✔
655

1✔
656
        if err != nil {
1✔
657
                return errors.Wrap(err, "Getting image metadata")
×
658
        }
×
659

660
        if found == nil {
1✔
661
                return ErrImageMetaNotFound
×
662
        }
×
663

664
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
665
        if err != nil {
1✔
666
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
667
        }
×
668

669
        // Image is in use, not allowed to delete
670
        if inUse {
2✔
671
                return ErrModelImageInActiveDeployment
1✔
672
        }
1✔
673

674
        // Delete image file (call to external service)
675
        // Noop for not existing file
676
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
677
        if err != nil {
1✔
678
                return err
×
679
        }
×
680
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
681
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
682
                return errors.Wrap(err, "Deleting image file")
×
683
        }
×
684

685
        // Delete metadata
686
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
687
                return errors.Wrap(err, "Deleting image metadata")
×
688
        }
×
689

690
        // update release
691
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
692
                return err
×
693
        }
×
694

695
        return nil
1✔
696
}
697

698
// ListImages according to specified filers.
699
func (d *Deployments) ListImages(
700
        ctx context.Context,
701
        filters *model.ReleaseOrImageFilter,
702
) ([]*model.Image, int, error) {
1✔
703
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
704
        if err != nil {
1✔
705
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
706
        }
×
707

708
        if imageList == nil {
2✔
709
                return make([]*model.Image, 0), 0, nil
1✔
710
        }
1✔
711

712
        return imageList, count, nil
1✔
713
}
714

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

×
719
        if err := constructor.Validate(); err != nil {
×
720
                return false, errors.Wrap(err, "Validating image metadata")
×
721
        }
×
722

723
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
724
        if err != nil {
×
725
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
726
        }
×
727

728
        if found {
×
729
                return false, ErrModelImageUsedInAnyDeployment
×
730
        }
×
731

732
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
733
        if err != nil {
×
734
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
735
        }
×
736

737
        if foundImage == nil {
×
738
                return false, nil
×
739
        }
×
740

741
        foundImage.SetModified(time.Now())
×
742
        foundImage.ImageMeta = constructor
×
743

×
744
        _, err = d.db.Update(ctx, foundImage)
×
745
        if err != nil {
×
746
                return false, errors.Wrap(err, "Updating image matadata")
×
747
        }
×
748

749
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
750
                return false, err
×
751
        }
×
752

753
        return true, nil
×
754
}
755

756
// DownloadLink presigned GET link to download image file.
757
// Returns error if image have not been uploaded.
758
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
759
        expire time.Duration) (*model.Link, error) {
1✔
760

1✔
761
        image, err := d.GetImage(ctx, imageID)
1✔
762
        if err != nil {
1✔
763
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
764
        }
×
765

766
        if image == nil {
1✔
767
                return nil, nil
×
768
        }
×
769

770
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
771
        if err != nil {
1✔
772
                return nil, err
×
773
        }
×
774
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
775
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
776
        if err != nil {
1✔
777
                return nil, errors.Wrap(err, "Searching for image file")
×
778
        }
×
779

780
        link, err := d.objectStorage.GetRequest(
1✔
781
                ctx,
1✔
782
                imagePath,
1✔
783
                image.Name+model.ArtifactFileSuffix,
1✔
784
                expire,
1✔
785
        )
1✔
786
        if err != nil {
1✔
787
                return nil, errors.Wrap(err, "Generating download link")
×
788
        }
×
789

790
        return link, nil
1✔
791
}
792

793
func (d *Deployments) UploadLink(
794
        ctx context.Context,
795
        expire time.Duration,
796
        skipVerify bool,
797
) (*model.UploadLink, error) {
6✔
798
        ctx, err := d.contextWithStorageSettings(ctx)
6✔
799
        if err != nil {
7✔
800
                return nil, err
1✔
801
        }
1✔
802

803
        artifactID := uuid.New().String()
5✔
804
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
5✔
805
        if skipVerify {
6✔
806
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
807
        }
1✔
808
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
5✔
809
        if err != nil {
6✔
810
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
1✔
811
        }
1✔
812
        upLink := &model.UploadLink{
4✔
813
                ArtifactID: artifactID,
4✔
814
                IssuedAt:   time.Now(),
4✔
815
                Link:       *link,
4✔
816
        }
4✔
817
        err = d.db.InsertUploadIntent(ctx, upLink)
4✔
818
        if err != nil {
5✔
819
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
1✔
820
        }
1✔
821

822
        return upLink, err
3✔
823
}
824

825
func (d *Deployments) processUploadedArtifact(
826
        ctx context.Context,
827
        artifactID string,
828
        artifact io.ReadCloser,
829
        skipVerify bool,
830
        metadata *model.DirectUploadMetadata,
831
) error {
5✔
832
        linkStatus := model.LinkStatusCompleted
5✔
833

5✔
834
        l := log.FromContext(ctx)
5✔
835
        defer artifact.Close()
5✔
836
        ctx, cancel := context.WithCancel(ctx)
5✔
837
        defer cancel()
5✔
838
        go func() { // Heatbeat routine
10✔
839
                ticker := time.NewTicker(inprogressIdleTime / 2)
5✔
840
                done := ctx.Done()
5✔
841
                defer ticker.Stop()
5✔
842
                for {
10✔
843
                        select {
5✔
844
                        case <-ticker.C:
×
845
                                err := d.db.UpdateUploadIntentStatus(
×
846
                                        ctx,
×
847
                                        artifactID,
×
848
                                        model.LinkStatusProcessing,
×
849
                                        model.LinkStatusProcessing,
×
850
                                )
×
851
                                if err != nil {
×
852
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
853
                                        cancel()
×
854
                                        return
×
855
                                }
×
856
                        case <-done:
5✔
857
                                return
5✔
858
                        }
859
                }
860
        }()
861
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
5✔
862
                ArtifactID:     artifactID,
5✔
863
                ArtifactReader: artifact,
5✔
864
        },
5✔
865
                skipVerify,
5✔
866
                metadata,
5✔
867
        )
5✔
868
        if err != nil {
9✔
869
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
4✔
870
                linkStatus = model.LinkStatusAborted
4✔
871
        }
4✔
872
        errDB := d.db.UpdateUploadIntentStatus(
5✔
873
                ctx, artifactID,
5✔
874
                model.LinkStatusProcessing, linkStatus,
5✔
875
        )
5✔
876
        if errDB != nil {
7✔
877
                l.Warnf("failed to update upload link status: %s", errDB)
2✔
878
        }
2✔
879
        return err
5✔
880
}
881

882
func (d *Deployments) CompleteUpload(
883
        ctx context.Context,
884
        intentID string,
885
        skipVerify bool,
886
        metadata *model.DirectUploadMetadata,
887
) error {
10✔
888
        l := log.FromContext(ctx)
10✔
889
        idty := identity.FromContext(ctx)
10✔
890
        ctx, err := d.contextWithStorageSettings(ctx)
10✔
891
        if err != nil {
11✔
892
                return err
1✔
893
        }
1✔
894
        // Create an async context that doesn't cancel when server connection
895
        // closes.
896
        ctxAsync := context.Background()
9✔
897
        ctxAsync = log.WithContext(ctxAsync, l)
9✔
898
        ctxAsync = identity.WithContext(ctxAsync, idty)
9✔
899

9✔
900
        settings, _ := storage.SettingsFromContext(ctx)
9✔
901
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
9✔
902
        var artifactReader io.ReadCloser
9✔
903
        if skipVerify {
12✔
904
                artifactReader, err = d.objectStorage.GetObject(
3✔
905
                        ctxAsync,
3✔
906
                        model.ImagePathFromContext(ctx, intentID),
3✔
907
                )
3✔
908
        } else {
9✔
909
                artifactReader, err = d.objectStorage.GetObject(
6✔
910
                        ctxAsync,
6✔
911
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
6✔
912
                )
6✔
913
        }
6✔
914
        if err != nil {
11✔
915
                if errors.Is(err, storage.ErrObjectNotFound) {
3✔
916
                        return ErrUploadNotFound
1✔
917
                }
1✔
918
                return err
1✔
919
        }
920

921
        err = d.db.UpdateUploadIntentStatus(
7✔
922
                ctx,
7✔
923
                intentID,
7✔
924
                model.LinkStatusPending,
7✔
925
                model.LinkStatusProcessing,
7✔
926
        )
7✔
927
        if err != nil {
9✔
928
                errClose := artifactReader.Close()
2✔
929
                if errClose != nil {
3✔
930
                        l.Warnf("failed to close artifact reader: %s", errClose)
1✔
931
                }
1✔
932
                if errors.Is(err, store.ErrNotFound) {
3✔
933
                        return ErrUploadNotFound
1✔
934
                }
1✔
935
                return err
1✔
936
        }
937
        go d.processUploadedArtifact( // nolint:errcheck
5✔
938
                ctxAsync, intentID, artifactReader, skipVerify, metadata,
5✔
939
        )
5✔
940
        return nil
5✔
941
}
942

943
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
944
        return &model.ArtifactInfo{
1✔
945
                Format:  info.Format,
1✔
946
                Version: uint(info.Version),
1✔
947
        }
1✔
948
}
1✔
949

950
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
951
        var files []model.UpdateFile
1✔
952
        for _, u := range uFiles {
2✔
953
                files = append(files, model.UpdateFile{
1✔
954
                        Name:     u.Name,
1✔
955
                        Size:     u.Size,
1✔
956
                        Date:     &u.Date,
1✔
957
                        Checksum: string(u.Checksum),
1✔
958
                })
1✔
959
        }
1✔
960
        return files, nil
1✔
961
}
962

963
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
5✔
964
        metaArtifact := model.NewArtifactMeta()
5✔
965

5✔
966
        aReader := areader.NewReader(*r)
5✔
967

5✔
968
        // There is no signature verification here.
5✔
969
        // It is just simple check if artifact is signed or not.
5✔
970
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
5✔
971
                metaArtifact.Signed = true
×
972
                return nil
×
973
        }
×
974

975
        var err error
5✔
976
        if skipVerify {
8✔
977
                err = aReader.ReadArtifactHeaders()
3✔
978
                if err != nil {
5✔
979
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
980
                }
2✔
981
        } else {
3✔
982
                err = aReader.ReadArtifact()
3✔
983
                if err != nil {
6✔
984
                        return nil, errors.Wrap(err, "reading artifact error")
3✔
985
                }
3✔
986
        }
987

988
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
989
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
990

1✔
991
        metaArtifact.Name = aReader.GetArtifactName()
1✔
992
        if metaArtifact.Info.Version == 3 {
2✔
993
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
994
                if err != nil {
1✔
995
                        return nil, errors.Wrap(err,
×
996
                                "error parsing version 3 artifact")
×
997
                }
×
998

999
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
1000
                if err != nil {
1✔
1001
                        return nil, errors.Wrap(err,
×
1002
                                "error parsing version 3 artifact")
×
1003
                }
×
1004

1005
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
1006
        }
1007

1008
        for _, p := range aReader.GetHandlers() {
2✔
1009
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
1010
                if err != nil {
1✔
1011
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
1012
                }
×
1013

1014
                uMetadata, err := p.GetUpdateMetaData()
1✔
1015
                if err != nil {
1✔
1016
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
1017
                }
×
1018

1019
                metaArtifact.Updates = append(
1✔
1020
                        metaArtifact.Updates,
1✔
1021
                        model.Update{
1✔
1022
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
1023
                                        Type: p.GetUpdateType(),
1✔
1024
                                },
1✔
1025
                                Files:    uFiles,
1✔
1026
                                MetaData: uMetadata,
1✔
1027
                        })
1✔
1028
        }
1029

1030
        return metaArtifact, nil
1✔
1031
}
1032

1033
func getArtifactIDs(artifacts []*model.Image) []string {
7✔
1034
        artifactIDs := make([]string, 0, len(artifacts))
7✔
1035
        for _, artifact := range artifacts {
14✔
1036
                artifactIDs = append(artifactIDs, artifact.Id)
7✔
1037
        }
7✔
1038
        return artifactIDs
7✔
1039
}
1040

1041
// deployments
1042
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
4✔
1043
        ids := make([]string, len(devices))
4✔
1044
        for i, d := range devices {
8✔
1045
                ids[i] = d.ID
4✔
1046
        }
4✔
1047

1048
        return ids
4✔
1049
}
1050

1051
// updateDeploymentConstructor fills devices list with device ids
1052
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
1053
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
5✔
1054
        l := log.FromContext(ctx)
5✔
1055

5✔
1056
        id := identity.FromContext(ctx)
5✔
1057
        if id == nil {
5✔
1058
                l.Error("identity not present in the context")
×
1059
                return nil, ErrModelInternal
×
1060
        }
×
1061
        searchParams := model.SearchParams{
5✔
1062
                Page:    1,
5✔
1063
                PerPage: PerPageInventoryDevices,
5✔
1064
                Filters: []model.FilterPredicate{
5✔
1065
                        {
5✔
1066
                                Scope:     InventoryIdentityScope,
5✔
1067
                                Attribute: InventoryStatusAttributeName,
5✔
1068
                                Type:      "$eq",
5✔
1069
                                Value:     InventoryStatusAccepted,
5✔
1070
                        },
5✔
1071
                },
5✔
1072
        }
5✔
1073
        if len(constructor.Group) > 0 {
10✔
1074
                searchParams.Filters = append(
5✔
1075
                        searchParams.Filters,
5✔
1076
                        model.FilterPredicate{
5✔
1077
                                Scope:     InventoryGroupScope,
5✔
1078
                                Attribute: InventoryGroupAttributeName,
5✔
1079
                                Type:      "$eq",
5✔
1080
                                Value:     constructor.Group,
5✔
1081
                        })
5✔
1082
        }
5✔
1083

1084
        for {
11✔
1085
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
6✔
1086
                if err != nil {
7✔
1087
                        l.Errorf("error searching for devices")
1✔
1088
                        return nil, ErrModelInternal
1✔
1089
                }
1✔
1090
                if count < 1 {
6✔
1091
                        l.Errorf("no devices found")
1✔
1092
                        return nil, ErrNoDevices
1✔
1093
                }
1✔
1094
                if len(devices) < 1 {
4✔
1095
                        break
×
1096
                }
1097
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
4✔
1098
                if len(constructor.Devices) == count {
7✔
1099
                        break
3✔
1100
                }
1101
                searchParams.Page++
1✔
1102
        }
1103

1104
        return constructor, nil
3✔
1105
}
1106

1107
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1108
func (d *Deployments) CreateDeviceConfigurationDeployment(
1109
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1110
        deviceID, deploymentID string) (string, error) {
5✔
1111

5✔
1112
        if constructor == nil {
6✔
1113
                return "", ErrModelMissingInput
1✔
1114
        }
1✔
1115

1116
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
4✔
1117
                constructor,
4✔
1118
                deploymentID,
4✔
1119
        )
4✔
1120
        if err != nil {
4✔
1121
                return "", errors.Wrap(err, "failed to create deployment")
×
1122
        }
×
1123

1124
        deployment.DeviceList = []string{deviceID}
4✔
1125
        deployment.MaxDevices = 1
4✔
1126
        deployment.Configuration = []byte(constructor.Configuration)
4✔
1127
        deployment.Type = model.DeploymentTypeConfiguration
4✔
1128

4✔
1129
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
4✔
1130
        if err != nil {
5✔
1131
                return "", err
1✔
1132
        }
1✔
1133
        deployment.Groups = groups
3✔
1134

3✔
1135
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
5✔
1136
                if strings.Contains(err.Error(), "duplicate key error") {
3✔
1137
                        return "", ErrDuplicateDeployment
1✔
1138
                }
1✔
1139
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
3✔
1140
                        return "", ErrInvalidDeploymentID
1✔
1141
                }
1✔
1142
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1143
        }
1144

1145
        return deployment.Id, nil
2✔
1146
}
1147

1148
// CreateDeployment precomputes new deployment and schedules it for devices.
1149
func (d *Deployments) CreateDeployment(ctx context.Context,
1150
        constructor *model.DeploymentConstructor) (string, error) {
9✔
1151

9✔
1152
        var err error
9✔
1153

9✔
1154
        if constructor == nil {
10✔
1155
                return "", ErrModelMissingInput
1✔
1156
        }
1✔
1157

1158
        if err := constructor.Validate(); err != nil {
8✔
1159
                return "", errors.Wrap(err, "Validating deployment")
×
1160
        }
×
1161

1162
        if len(constructor.Group) > 0 || constructor.AllDevices {
13✔
1163
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
5✔
1164
                if err != nil {
7✔
1165
                        return "", err
2✔
1166
                }
2✔
1167
        }
1168

1169
        deployment, err := model.NewDeploymentFromConstructor(constructor)
6✔
1170
        if err != nil {
6✔
1171
                return "", errors.Wrap(err, "failed to create deployment")
×
1172
        }
×
1173

1174
        // Assign artifacts to the deployment.
1175
        // When new artifact(s) with the artifact name same as the one in the deployment
1176
        // will be uploaded to the backend, it will also become part of this deployment.
1177
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
6✔
1178
        if err != nil {
6✔
1179
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1180
        }
×
1181

1182
        if len(artifacts) == 0 {
7✔
1183
                return "", ErrNoArtifact
1✔
1184
        }
1✔
1185

1186
        deployment.Artifacts = getArtifactIDs(artifacts)
6✔
1187
        deployment.DeviceList = constructor.Devices
6✔
1188
        deployment.MaxDevices = len(constructor.Devices)
6✔
1189
        deployment.Type = model.DeploymentTypeSoftware
6✔
1190
        if len(constructor.Group) > 0 {
9✔
1191
                deployment.Groups = []string{constructor.Group}
3✔
1192
        }
3✔
1193

1194
        // single device deployment case
1195
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
9✔
1196
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
3✔
1197
                if err != nil {
3✔
1198
                        return "", err
×
1199
                }
×
1200
                deployment.Groups = groups
3✔
1201
        }
1202

1203
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
7✔
1204
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1205
        }
1✔
1206

1207
        return deployment.Id, nil
5✔
1208
}
1209

1210
func (d *Deployments) getDeploymentGroups(
1211
        ctx context.Context,
1212
        devices []string,
1213
) ([]string, error) {
6✔
1214
        id := identity.FromContext(ctx)
6✔
1215

6✔
1216
        //only for single device deployment case
6✔
1217
        if len(devices) != 1 {
6✔
1218
                return nil, nil
×
1219
        }
×
1220

1221
        if id == nil {
7✔
1222
                id = &identity.Identity{}
1✔
1223
        }
1✔
1224

1225
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
6✔
1226
        if err != nil && err != inventory.ErrDevNotFound {
7✔
1227
                return nil, err
1✔
1228
        }
1✔
1229
        return groups, nil
5✔
1230
}
1231

1232
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1233
func (d *Deployments) IsDeploymentFinished(
1234
        ctx context.Context,
1235
        deploymentID string,
1236
) (bool, error) {
1✔
1237
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1238
        if err != nil {
1✔
1239
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1240
        }
×
1241
        if deployment == nil {
2✔
1242
                return true, nil
1✔
1243
        }
1✔
1244

1245
        return false, nil
1✔
1246
}
1247

1248
// GetDeployment fetches deployment by ID
1249
func (d *Deployments) GetDeployment(ctx context.Context,
1250
        deploymentID string) (*model.Deployment, error) {
1✔
1251

1✔
1252
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1253
        if err != nil {
1✔
1254
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1255
        }
×
1256

1257
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1258
                return nil, err
×
1259
        }
×
1260

1261
        return deployment, nil
1✔
1262
}
1263

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

4✔
1269
        var found bool
4✔
1270

4✔
1271
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
4✔
1272
        if err != nil {
5✔
1273
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1274
        }
1✔
1275

1276
        if found {
4✔
1277
                return found, nil
1✔
1278
        }
1✔
1279

1280
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
3✔
1281
                imageID, model.ActiveDeploymentStatuses()...)
3✔
1282
        if err != nil {
4✔
1283
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1284
        }
1✔
1285

1286
        return found, nil
2✔
1287
}
1288

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

×
1293
        var found bool
×
1294

×
1295
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1296
        if err != nil {
×
1297
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1298
        }
×
1299

1300
        if found {
×
1301
                return found, nil
×
1302
        }
×
1303

1304
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
1305
        if err != nil {
×
1306
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
1307
        }
×
1308

1309
        return found, nil
×
1310
}
1311

1312
// Retrieves the model.Deployment and model.DeviceDeployment structures
1313
// for the device. Upon error, nil is returned for both deployment structures.
1314
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1315
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1316

2✔
1317
        // Retrieve device deployment
2✔
1318
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
2✔
1319

2✔
1320
        if err != nil {
2✔
1321
                return nil, nil, errors.Wrap(err,
×
1322
                        "Searching for oldest active deployment for the device")
×
1323
        } else if deviceDeployment == nil {
3✔
1324
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1325
        }
1✔
1326

1327
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1328
        if err != nil {
2✔
1329
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1330
        }
×
1331
        if deployment == nil {
2✔
1332
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1333
        }
×
1334

1335
        return deployment, deviceDeployment, nil
2✔
1336
}
1337

1338
// getNewDeploymentForDevice returns deployment object and creates and returns
1339
// new device deployment for the device;
1340
//
1341
// we are interested only in the deployments that are newer than the latest
1342
// deployment applied by the device;
1343
// this way we guarantee that the device will not receive deployment
1344
// that is older than the one installed on the device;
1345
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1346
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1347

1✔
1348
        var lastDeployment *time.Time
1✔
1349
        //get latest device deployment for the device;
1✔
1350
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1351
        if err != nil {
1✔
1352
                return nil, nil, errors.Wrap(err,
×
1353
                        "Searching for latest active deployment for the device")
×
1354
        } else if deviceDeployment == nil {
2✔
1355
                lastDeployment = &time.Time{}
1✔
1356
        } else {
2✔
1357
                lastDeployment = deviceDeployment.Created
1✔
1358
        }
1✔
1359

1360
        //get deployments newer then last device deployment
1361
        //iterate over deployments and check if the device is part of the deployment or not
1362
        for skip := 0; true; skip += 100 {
2✔
1363
                deployments, err := d.db.FindNewerActiveDeployments(ctx, lastDeployment, skip, 100)
1✔
1364
                if err != nil {
1✔
1365
                        return nil, nil, errors.Wrap(err,
×
1366
                                "Failed to search for newer active deployments")
×
1367
                }
×
1368
                if len(deployments) == 0 {
2✔
1369
                        return nil, nil, nil
1✔
1370
                }
1✔
1371

1372
                for _, deployment := range deployments {
2✔
1373
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1374
                        if err != nil {
1✔
1375
                                return nil, nil, err
×
1376
                        }
×
1377
                        if ok {
2✔
1378
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1379
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1380
                                if err != nil {
1✔
1381
                                        return nil, nil, err
×
1382
                                }
×
1383
                                return deployment, deviceDeployment, nil
1✔
1384
                        }
1385
                }
1386
        }
1387

1388
        return nil, nil, nil
×
1389
}
1390

1391
func (d *Deployments) createDeviceDeploymentWithStatus(
1392
        ctx context.Context, deviceID string,
1393
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1394
) (*model.DeviceDeployment, error) {
6✔
1395
        prevStatus := model.DeviceDeploymentStatusNull
6✔
1396
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
6✔
1397
        if err != nil && err != mongo.ErrStorageNotFound {
6✔
1398
                return nil, err
×
1399
        } else if deviceDeployment != nil {
6✔
1400
                prevStatus = deviceDeployment.Status
×
1401
        }
×
1402

1403
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
6✔
1404
        deviceDeployment.Status = status
6✔
1405
        deviceDeployment.Active = status.Active()
6✔
1406
        deviceDeployment.Created = deployment.Created
6✔
1407

6✔
1408
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
6✔
1409
                return nil, err
×
1410
        }
×
1411

1412
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
6✔
1413
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
6✔
1414
                return nil, err
×
1415
        }
×
1416

1417
        // after inserting new device deployment update deployment stats
1418
        // in the database and locally, and update deployment status
1419
        if err := d.db.UpdateStatsInc(
6✔
1420
                ctx, deployment.Id,
6✔
1421
                prevStatus, status,
6✔
1422
        ); err != nil {
6✔
1423
                return nil, err
×
1424
        }
×
1425

1426
        deployment.Stats.Inc(status)
6✔
1427

6✔
1428
        err = d.recalcDeploymentStatus(ctx, deployment)
6✔
1429
        if err != nil {
6✔
1430
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1431
        }
×
1432

1433
        if !status.Active() {
11✔
1434
                err := d.reindexDevice(ctx, deviceID)
5✔
1435
                if err != nil {
5✔
1436
                        l := log.FromContext(ctx)
×
1437
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1438
                }
×
1439
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1440
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1441
                        l := log.FromContext(ctx)
×
1442
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1443
                }
×
1444
        }
1445

1446
        return deviceDeployment, nil
6✔
1447
}
1448

1449
func (d *Deployments) isDevicePartOfDeployment(
1450
        ctx context.Context,
1451
        deviceID string,
1452
        deployment *model.Deployment,
1453
) (bool, error) {
8✔
1454
        for _, id := range deployment.DeviceList {
14✔
1455
                if id == deviceID {
12✔
1456
                        return true, nil
6✔
1457
                }
6✔
1458
        }
1459
        return false, nil
3✔
1460
}
1461

1462
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1463
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1464
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
2✔
1465

2✔
1466
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
2✔
1467
        if err != nil {
2✔
1468
                return nil, ErrModelInternal
×
1469
        } else if deployment == nil {
3✔
1470
                return nil, nil
1✔
1471
        }
1✔
1472

1473
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
2✔
1474
        if err != nil {
3✔
1475
                return nil, err
1✔
1476
        }
1✔
1477
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
2✔
1478
}
1479

1480
func (d *Deployments) getDeploymentInstructions(
1481
        ctx context.Context,
1482
        deployment *model.Deployment,
1483
        deviceDeployment *model.DeviceDeployment,
1484
        request *model.DeploymentNextRequest,
1485
) (*model.DeploymentInstructions, error) {
2✔
1486

2✔
1487
        var newArtifactAssigned bool
2✔
1488

2✔
1489
        l := log.FromContext(ctx)
2✔
1490

2✔
1491
        if deployment.Type == model.DeploymentTypeConfiguration {
3✔
1492
                // There's nothing more we need to do, the link must be filled
1✔
1493
                // in by the API layer.
1✔
1494
                return &model.DeploymentInstructions{
1✔
1495
                        ID: deployment.Id,
1✔
1496
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1497
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1498
                                // use deployment ID togheter with device ID as artifact ID
1✔
1499
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1500
                                ArtifactName:          deployment.ArtifactName,
1✔
1501
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1502
                        },
1✔
1503
                        Type: model.DeploymentTypeConfiguration,
1✔
1504
                }, nil
1✔
1505
        }
1✔
1506

1507
        // assing artifact to the device deployment
1508
        // only if it was not assgined previously
1509
        if deviceDeployment.Image == nil {
4✔
1510
                if err := d.assignArtifact(
2✔
1511
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
2✔
1512
                        return nil, err
×
1513
                }
×
1514
                newArtifactAssigned = true
2✔
1515
        }
1516

1517
        if deviceDeployment.Image == nil {
2✔
1518
                // No artifact - return empty response
×
1519
                return nil, nil
×
1520
        }
×
1521

1522
        // if the deployment is not forcing the installation, and
1523
        // if artifact was recognized as already installed, and this is
1524
        // a new device deployment - indicated by device deployment status "pending",
1525
        // handle already installed artifact case
1526
        if !deployment.ForceInstallation &&
2✔
1527
                d.isAlreadyInstalled(request, deviceDeployment) &&
2✔
1528
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
4✔
1529
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
2✔
1530
        }
2✔
1531

1532
        // if new artifact has been assigned to device deployment
1533
        // add artifact size to deployment total size,
1534
        // before returning deployment instruction to the device
1535
        if newArtifactAssigned {
2✔
1536
                if err := d.db.IncrementDeploymentTotalSize(
1✔
1537
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
1538
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1539
                }
×
1540
        }
1541

1542
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1543
        if err != nil {
1✔
1544
                return nil, err
×
1545
        }
×
1546

1547
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1548
        link, err := d.objectStorage.GetRequest(
1✔
1549
                ctx,
1✔
1550
                imagePath,
1✔
1551
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1552
                DefaultUpdateDownloadLinkExpire,
1✔
1553
        )
1✔
1554
        if err != nil {
1✔
1555
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1556
        }
×
1557

1558
        instructions := &model.DeploymentInstructions{
1✔
1559
                ID: deviceDeployment.DeploymentId,
1✔
1560
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1561
                        ID: deviceDeployment.Image.Id,
1✔
1562
                        ArtifactName: deviceDeployment.Image.
1✔
1563
                                ArtifactMeta.Name,
1✔
1564
                        Source: *link,
1✔
1565
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1566
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1567
                },
1✔
1568
        }
1✔
1569

1✔
1570
        return instructions, nil
1✔
1571
}
1572

1573
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1574
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
2✔
1575
        if deviceDeployment.Request != nil {
3✔
1576
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1577
                        // the device reported different device type and/or artifact name
1✔
1578
                        // during the update process, which should never happen;
1✔
1579
                        // mark deployment for this device as failed to force client to rollback
1✔
1580
                        l := log.FromContext(ctx)
1✔
1581
                        l.Errorf(
1✔
1582
                                "Device with id %s reported new data: %s during update process;"+
1✔
1583
                                        "old data: %s",
1✔
1584
                                deviceID, request, deviceDeployment.Request)
1✔
1585

1✔
1586
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1587
                                model.DeviceDeploymentState{
1✔
1588
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1589
                                }); err != nil {
1✔
1590
                                return errors.Wrap(err, "Failed to update deployment status")
×
1591
                        }
×
1592
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1593
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1594
                        }
×
1595
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1596
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1597
                                l := log.FromContext(ctx)
×
1598
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1599
                        }
×
1600
                        return ErrConflictingRequestData
1✔
1601
                }
1602
        } else {
2✔
1603
                // save the request
2✔
1604
                if err := d.db.SaveDeviceDeploymentRequest(
2✔
1605
                        ctx, deviceDeployment.Id, request); err != nil {
2✔
1606
                        return err
×
1607
                }
×
1608
        }
1609
        return nil
2✔
1610
}
1611

1612
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1613
// ID `deviceID`. Returns nil if update was successful.
1614
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1615
        deviceID string, ddState model.DeviceDeploymentState) error {
6✔
1616

6✔
1617
        l := log.FromContext(ctx)
6✔
1618

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

6✔
1621
        var finishTime *time.Time = nil
6✔
1622
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
10✔
1623
                now := time.Now()
4✔
1624
                finishTime = &now
4✔
1625
        }
4✔
1626

1627
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
6✔
1628
        if err == mongo.ErrStorageNotFound {
7✔
1629
                return ErrStorageNotFound
1✔
1630
        } else if err != nil {
6✔
1631
                return err
×
1632
        }
×
1633

1634
        currentStatus := dd.Status
5✔
1635

5✔
1636
        if currentStatus == model.DeviceDeploymentStatusAborted {
5✔
1637
                return ErrDeploymentAborted
×
1638
        }
×
1639

1640
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
5✔
1641
                return ErrDeviceDecommissioned
×
1642
        }
×
1643

1644
        // nothing to do
1645
        if ddState.Status == currentStatus {
5✔
1646
                return nil
×
1647
        }
×
1648

1649
        // update finish time
1650
        ddState.FinishTime = finishTime
5✔
1651

5✔
1652
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
5✔
1653
                deviceID, deploymentID, ddState)
5✔
1654
        if err != nil {
5✔
1655
                return err
×
1656
        }
×
1657

1658
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
5✔
1659
                return err
×
1660
        }
×
1661

1662
        // fetch deployment stats and update deployment status
1663
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
1664
        if err != nil {
5✔
1665
                return errors.Wrap(err, "failed when searching for deployment")
×
1666
        }
×
1667

1668
        err = d.recalcDeploymentStatus(ctx, deployment)
5✔
1669
        if err != nil {
5✔
1670
                return errors.Wrap(err, "failed to update deployment status")
×
1671
        }
×
1672

1673
        if !ddState.Status.Active() {
9✔
1674
                l := log.FromContext(ctx)
4✔
1675
                ldd := model.DeviceDeployment{
4✔
1676
                        DeviceId:     dd.DeviceId,
4✔
1677
                        DeploymentId: dd.DeploymentId,
4✔
1678
                        Id:           dd.Id,
4✔
1679
                        Status:       ddState.Status,
4✔
1680
                }
4✔
1681
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
4✔
1682
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1683
                }
×
1684
                if err := d.reindexDevice(ctx, deviceID); err != nil {
4✔
1685
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1686
                }
×
1687
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
4✔
1688
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1689
                }
×
1690
        }
1691

1692
        return nil
5✔
1693
}
1694

1695
// recalcDeploymentStatus inspects the deployment stats and
1696
// recalculates and updates its status
1697
// it should be used whenever deployment stats are touched
1698
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
10✔
1699
        status := dep.GetStatus()
10✔
1700

10✔
1701
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
10✔
1702
                return err
×
1703
        }
×
1704

1705
        return nil
10✔
1706
}
1707

1708
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1709
        deploymentID string) (model.Stats, error) {
1✔
1710

1✔
1711
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1712

1✔
1713
        if err != nil {
1✔
1714
                return nil, errors.Wrap(err, "checking deployment id")
×
1715
        }
×
1716

1717
        if deployment == nil {
1✔
1718
                return nil, nil
×
1719
        }
×
1720

1721
        return deployment.Stats, nil
1✔
1722
}
1723
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1724
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1725

×
1726
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1727

×
1728
        if err != nil {
×
1729
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1730
        }
×
1731

1732
        if deploymentStats == nil {
×
1733
                return nil, ErrModelDeploymentNotFound
×
1734
        }
×
1735

1736
        return deploymentStats, nil
×
1737
}
1738

1739
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1740
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1741
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1742

1✔
1743
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1744
        if err != nil {
1✔
1745
                return nil, ErrModelInternal
×
1746
        }
×
1747

1748
        if deployment == nil {
1✔
1749
                return nil, ErrModelDeploymentNotFound
×
1750
        }
×
1751

1752
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1753
        if err != nil {
1✔
1754
                return nil, ErrModelInternal
×
1755
        }
×
1756

1757
        return statuses, nil
1✔
1758
}
1759

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

1✔
1763
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1764
        if err != nil {
1✔
1765
                return nil, -1, ErrModelInternal
×
1766
        }
×
1767

1768
        if deployment == nil {
1✔
1769
                return nil, -1, ErrModelDeploymentNotFound
×
1770
        }
×
1771

1772
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1773
        if err != nil {
1✔
1774
                return nil, -1, ErrModelInternal
×
1775
        }
×
1776

1777
        return statuses, totalCount, nil
1✔
1778
}
1779

1780
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1781
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
4✔
1782
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
4✔
1783
        if err != nil {
5✔
1784
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1785
        }
1✔
1786

1787
        deploymentIDs := make([]string, len(deviceDeployments))
3✔
1788
        for i, deviceDeployment := range deviceDeployments {
9✔
1789
                deploymentIDs[i] = deviceDeployment.DeploymentId
6✔
1790
        }
6✔
1791
        var deployments []*model.Deployment
3✔
1792
        if len(deviceDeployments) > 0 {
6✔
1793
                deployments, _, err = d.db.Find(ctx, model.Query{
3✔
1794
                        IDs:          deploymentIDs,
3✔
1795
                        Limit:        len(deviceDeployments),
3✔
1796
                        DisableCount: true,
3✔
1797
                })
3✔
1798
                if err != nil {
4✔
1799
                        return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1800
                }
1✔
1801
        }
1802

1803
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1804
        for _, deployment := range deployments {
5✔
1805
                deploymentsMap[deployment.Id] = deployment
3✔
1806
        }
3✔
1807

1808
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1809
        for i, deviceDeployment := range deviceDeployments {
6✔
1810
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
7✔
1811
                        res = append(res, model.DeviceDeploymentListItem{
3✔
1812
                                Id:         deviceDeployment.Id,
3✔
1813
                                Deployment: deployment,
3✔
1814
                                Device:     &deviceDeployments[i],
3✔
1815
                        })
3✔
1816
                } else {
4✔
1817
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1818
                                Id:     deviceDeployment.Id,
1✔
1819
                                Device: &deviceDeployments[i],
1✔
1820
                        })
1✔
1821
                }
1✔
1822
        }
1823

1824
        return res, totalCount, nil
2✔
1825
}
1826

1827
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1828
        ctx context.Context,
1829
        deployment *model.Deployment,
1830
) error {
6✔
1831
        if deployment.DeviceCount == nil {
6✔
1832
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1833
                if err != nil {
×
1834
                        return errors.Wrap(err, "counting device deployments")
×
1835
                }
×
1836
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1837
                if err != nil {
×
1838
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1839
                }
×
1840
                deployment.DeviceCount = &deviceCount
×
1841
        }
1842

1843
        return nil
6✔
1844
}
1845

1846
func (d *Deployments) LookupDeployment(ctx context.Context,
1847
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1848
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1849

1✔
1850
        if err != nil {
1✔
1851
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1852
        }
×
1853

1854
        if list == nil {
2✔
1855
                return make([]*model.Deployment, 0), 0, nil
1✔
1856
        }
1✔
1857

1858
        for _, deployment := range list {
×
1859
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1860
                        return nil, 0, err
×
1861
                }
×
1862
        }
1863

1864
        return list, totalCount, nil
×
1865
}
1866

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

1✔
1872
        // repack to temporary deployment log and validate
1✔
1873
        dlog := model.DeploymentLog{
1✔
1874
                DeviceID:     deviceID,
1✔
1875
                DeploymentID: deploymentID,
1✔
1876
                Messages:     logs,
1✔
1877
        }
1✔
1878
        if err := dlog.Validate(); err != nil {
1✔
1879
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1880
        }
×
1881

1882
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1883
                if err != nil {
×
1884
                        return err
×
1885
                } else {
×
1886
                        return ErrModelDeploymentNotFound
×
1887
                }
×
1888
        }
1889

1890
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1891
                return err
×
1892
        }
×
1893

1894
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1895
                deviceID, deploymentID, true)
1✔
1896
}
1897

1898
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1899
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1900

1✔
1901
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1902
                deviceID, deploymentID)
1✔
1903
}
1✔
1904

1905
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1906
        deploymentID string, deviceID string) (bool, error) {
1✔
1907
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1908
}
1✔
1909

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

5✔
1913
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
6✔
1914
                return err
1✔
1915
        }
1✔
1916

1917
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
4✔
1918
                ctx, deploymentID)
4✔
1919
        if err != nil {
5✔
1920
                return err
1✔
1921
        }
1✔
1922

1923
        // update statistics
1924
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
4✔
1925
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1926
        }
1✔
1927

1928
        // when aborting the deployment we need to set status directly instead of
1929
        // using recalcDeploymentStatus method;
1930
        // it is possible that the deployment does not have any device deployments yet;
1931
        // in that case, all statistics are 0 and calculating status based on statistics
1932
        // will not work - the calculated status will be "pending"
1933
        if err := d.db.SetDeploymentStatus(ctx,
2✔
1934
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
2✔
1935
                return errors.Wrap(err, "failed to update deployment status")
×
1936
        }
×
1937

1938
        return nil
2✔
1939
}
1940

1941
func (d *Deployments) updateDeviceDeploymentsStatus(
1942
        ctx context.Context,
1943
        deviceId string,
1944
        status model.DeviceDeploymentStatus,
1945
) error {
15✔
1946
        var latestDeployment *time.Time
15✔
1947
        // Retrieve active device deployment for the device
15✔
1948
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
15✔
1949
        if err != nil {
17✔
1950
                return errors.Wrap(err, "Searching for active deployment for the device")
2✔
1951
        } else if deviceDeployment != nil {
17✔
1952
                now := time.Now()
2✔
1953
                ddStatus := model.DeviceDeploymentState{
2✔
1954
                        Status:     status,
2✔
1955
                        FinishTime: &now,
2✔
1956
                }
2✔
1957
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
2✔
1958
                        deviceId, ddStatus); err != nil {
2✔
1959
                        return errors.Wrap(err, "updating device deployment status")
×
1960
                }
×
1961
                latestDeployment = deviceDeployment.Created
2✔
1962
        } else {
11✔
1963
                // get latest device deployment for the device
11✔
1964
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
11✔
1965
                if err != nil {
11✔
1966
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1967
                } else if deviceDeployment == nil {
20✔
1968
                        latestDeployment = &time.Time{}
9✔
1969
                } else {
11✔
1970
                        latestDeployment = deviceDeployment.Created
2✔
1971
                }
2✔
1972
        }
1973

1974
        // get deployments newer then last device deployment
1975
        // iterate over deployments and check if the device is part of the deployment or not
1976
        // if the device is part of the deployment create new, decommisioned device deployment
1977
        for skip := 0; true; skip += 100 {
33✔
1978
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
20✔
1979
                if err != nil {
20✔
1980
                        return errors.Wrap(err, "Failed to search for newer active deployments")
×
1981
                }
×
1982
                if len(deployments) == 0 {
33✔
1983
                        break
13✔
1984
                }
1985
                for _, deployment := range deployments {
14✔
1986
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
7✔
1987
                        if err != nil {
7✔
1988
                                return err
×
1989
                        }
×
1990
                        if ok {
12✔
1991
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
5✔
1992
                                        deviceId, deployment, status)
5✔
1993
                                if err != nil {
5✔
1994
                                        return err
×
1995
                                }
×
1996
                                if !status.Active() {
10✔
1997
                                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1998
                                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1999
                                                l := log.FromContext(ctx)
×
2000
                                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
2001
                                        }
×
2002
                                }
2003
                        }
2004
                }
2005
        }
2006

2007
        if err := d.reindexDevice(ctx, deviceId); err != nil {
13✔
2008
                l := log.FromContext(ctx)
×
2009
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
2010
        }
×
2011

2012
        return nil
13✔
2013
}
2014

2015
// DecommissionDevice updates the status of all the pending and active deployments for a device
2016
// to decommissioned
2017
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
7✔
2018
        return d.updateDeviceDeploymentsStatus(
7✔
2019
                ctx,
7✔
2020
                deviceId,
7✔
2021
                model.DeviceDeploymentStatusDecommissioned,
7✔
2022
        )
7✔
2023
}
7✔
2024

2025
// AbortDeviceDeployments aborts all the pending and active deployments for a device
2026
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
8✔
2027
        return d.updateDeviceDeploymentsStatus(
8✔
2028
                ctx,
8✔
2029
                deviceId,
8✔
2030
                model.DeviceDeploymentStatusAborted,
8✔
2031
        )
8✔
2032
}
8✔
2033

2034
// DeleteDeviceDeploymentsHistory deletes the device deployments history
2035
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
2✔
2036
        // get device deployments which will be marked as deleted
2✔
2037
        f := false
2✔
2038
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
2✔
2039
        if err != nil {
2✔
2040
                return err
×
2041
        }
×
2042

2043
        // no device deployments to update
2044
        if len(dd) <= 0 {
2✔
2045
                return nil
×
2046
        }
×
2047

2048
        // mark device deployments as deleted
2049
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
3✔
2050
                return err
1✔
2051
        }
1✔
2052

2053
        // trigger reindexing of updated device deployments
2054
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2055
        for i, d := range dd {
2✔
2056
                deviceDeployments[i].ID = d.Id
1✔
2057
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2058
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2059
        }
1✔
2060
        if d.reportingClient != nil {
2✔
2061
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2062
        }
1✔
2063
        return err
1✔
2064
}
2065

2066
// Storage settings
2067
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
2068
        settings, err := d.db.GetStorageSettings(ctx)
3✔
2069
        if err != nil {
4✔
2070
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2071
        }
1✔
2072

2073
        return settings, nil
2✔
2074
}
2075

2076
func (d *Deployments) SetStorageSettings(
2077
        ctx context.Context,
2078
        storageSettings *model.StorageSettings,
2079
) error {
4✔
2080
        if storageSettings != nil {
8✔
2081
                ctx = storage.SettingsWithContext(ctx, storageSettings)
4✔
2082
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
4✔
2083
                        return errors.WithMessage(err,
×
2084
                                "the provided storage settings failed the health check",
×
2085
                        )
×
2086
                }
×
2087
        }
2088
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
6✔
2089
                return errors.Wrap(err, "Failed to save settings")
2✔
2090
        }
2✔
2091

2092
        return nil
2✔
2093
}
2094

2095
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
8✔
2096
        d.reportingClient = c
8✔
2097
        return d
8✔
2098
}
8✔
2099

2100
func (d *Deployments) haveReporting() bool {
6✔
2101
        return d.reportingClient != nil
6✔
2102
}
6✔
2103

2104
func (d *Deployments) search(
2105
        ctx context.Context,
2106
        tid string,
2107
        parms model.SearchParams,
2108
) ([]model.InvDevice, int, error) {
6✔
2109
        if d.haveReporting() {
7✔
2110
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2111
        } else {
6✔
2112
                return d.inventoryClient.Search(ctx, tid, parms)
5✔
2113
        }
5✔
2114
}
2115

2116
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2117
        ctx context.Context,
2118
        artifactName string,
2119
) error {
2✔
2120
        // first check if there are pending deployments with given artifact name
2✔
2121
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
2✔
2122
        if err != nil {
2✔
2123
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
2124
        }
×
2125
        if !exists {
3✔
2126
                return nil
1✔
2127
        }
1✔
2128

2129
        // Assign artifacts to the deployments with given artifact name
2130
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2131
        if err != nil {
1✔
2132
                return errors.Wrap(err, "Finding artifact with given name")
×
2133
        }
×
2134

2135
        if len(artifacts) == 0 {
1✔
2136
                return ErrNoArtifact
×
2137
        }
×
2138
        artifactIDs := getArtifactIDs(artifacts)
1✔
2139
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2140
}
2141

2142
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
25✔
2143
        if d.reportingClient != nil {
28✔
2144
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
3✔
2145
        }
3✔
2146
        return nil
22✔
2147
}
2148

2149
func (d *Deployments) reindexDeployment(ctx context.Context,
2150
        deviceID, deploymentID, ID string) error {
17✔
2151
        if d.reportingClient != nil {
20✔
2152
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
3✔
2153
        }
3✔
2154
        return nil
14✔
2155
}
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