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

mendersoftware / deployments / 1062206731

06 Nov 2023 10:31AM UTC coverage: 77.615% (-3.3%) from 80.909%
1062206731

push

gitlab-ci

web-flow
Merge pull request #948 from alfrunes/upgrade-deps

Upgrade go mod dependencies

13 of 25 new or added lines in 4 files covered. (52.0%)

5 existing lines in 2 files now uncovered.

4133 of 5325 relevant lines covered (77.62%)

63.14 hits per line

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

77.12
/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
                if err != nil {
24✔
275
                        return nil, err
2✔
276
                }
2✔
277
        }
278
        if settings != nil {
24✔
NEW
279
                if settings.UseAccelerate && settings.Uri != "" {
×
NEW
280
                        log.FromContext(ctx).
×
NEW
281
                                Warn(`storage settings: custom "uri" and "use_accelerate" ` +
×
NEW
282
                                        `are not allowed: disabling transfer acceleration`)
×
NEW
283
                        settings.UseAccelerate = false
×
NEW
284
                }
×
285
                err = settings.Validate()
×
286
                if err != nil {
×
287
                        return nil, err
×
288
                }
×
289
        }
290
        return storage.SettingsWithContext(ctx, settings), nil
24✔
291
}
292

293
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
3✔
294
        limit, err := d.db.GetLimit(ctx, name)
3✔
295
        if err == mongo.ErrLimitNotFound {
4✔
296
                return &model.Limit{
1✔
297
                        Name:  name,
1✔
298
                        Value: 0,
1✔
299
                }, nil
1✔
300

1✔
301
        } else if err != nil {
4✔
302
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
303
        }
1✔
304
        return limit, nil
1✔
305
}
306

307
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
3✔
308
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
4✔
309
                return errors.Wrap(err, "failed to provision tenant")
1✔
310
        }
1✔
311

312
        return nil
2✔
313
}
314

315
// CreateImage parses artifact and uploads artifact file to the file storage - in parallel,
316
// and creates image structure in the system.
317
// Returns image ID and nil on success.
318
func (d *Deployments) CreateImage(ctx context.Context,
319
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
1✔
320
        return d.handleArtifact(ctx, multipartUploadMsg, false, nil)
1✔
321
}
1✔
322

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

345
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
346
// and creates image structure in the system.
347
// Returns image ID, artifact file ID and nil on success.
348
func (d *Deployments) handleArtifact(ctx context.Context,
349
        multipartUploadMsg *model.MultipartUploadMsg,
350
        skipVerify bool,
351
        metadata *model.DirectUploadMetadata,
352
) (string, error) {
5✔
353

5✔
354
        l := log.FromContext(ctx)
5✔
355
        ctx, err := d.contextWithStorageSettings(ctx)
5✔
356
        if err != nil {
5✔
357
                return "", err
×
358
        }
×
359

360
        // create pipe
361
        pR, pW := io.Pipe()
5✔
362

5✔
363
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
5✔
364

5✔
365
        tee := io.TeeReader(artifactReader, pW)
5✔
366

5✔
367
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
5✔
368
        if err != nil {
6✔
369
                uid, _ = uuid.NewRandom()
1✔
370
        }
1✔
371
        artifactID := uid.String()
5✔
372

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

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

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

432
        // close the pipe
433
        pW.Close()
1✔
434

1✔
435
        // collect output from the goroutine
1✔
436
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
437
                return artifactID, uploadResponseErr
×
438
        }
×
439

440
        size := artifactReader.Count()
1✔
441
        if skipVerify && validMetadata {
2✔
442
                size = metadata.Size
1✔
443
        }
1✔
444
        image := model.NewImage(
1✔
445
                artifactID,
1✔
446
                multipartUploadMsg.MetaConstructor,
1✔
447
                metaArtifactConstructor,
1✔
448
                size,
1✔
449
        )
1✔
450

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

1✔
469
        // update release
1✔
470
        if err := d.updateRelease(ctx, image, nil); err != nil {
1✔
471
                return "", err
×
472
        }
×
473

474
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
475
                return "", errors.Wrap(err, "fail to update deployments")
×
476
        }
×
477

478
        return artifactID, nil
1✔
479
}
480

481
func validUpdates(constructorUpdates []model.Update, metadataUpdates []model.Update) bool {
1✔
482
        valid := false
1✔
483
        if len(constructorUpdates) == len(metadataUpdates) {
2✔
484
                valid = true
1✔
485
                for _, update := range constructorUpdates {
2✔
486
                        for _, updateExternal := range metadataUpdates {
2✔
487
                                if !update.Match(updateExternal) {
1✔
488
                                        valid = false
×
489
                                        break
×
490
                                }
491
                        }
492
                }
493
        }
494
        return valid
1✔
495
}
496

497
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
498
// creates image structure in the system, and starts the workflow to generate the
499
// artifact from them.
500
// Returns image ID and nil on success.
501
func (d *Deployments) GenerateImage(ctx context.Context,
502
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
11✔
503

11✔
504
        if multipartGenerateImageMsg == nil {
12✔
505
                return "", ErrModelMultipartUploadMsgMalformed
1✔
506
        }
1✔
507

508
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
10✔
509
        if err != nil {
15✔
510
                return "", err
5✔
511
        }
5✔
512
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
6✔
513
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
514
        }
1✔
515
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
5✔
516
        if err != nil {
7✔
517
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
3✔
518
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
519
                }
1✔
520
                return "", err
1✔
521
        }
522

523
        return multipartGenerateImageMsg.ArtifactID, err
3✔
524
}
525

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

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

2✔
569
        return &buf, err
2✔
570
}
571

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

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

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

617
        link, err := d.objectStorage.GetRequest(
7✔
618
                ctx,
7✔
619
                filePath,
7✔
620
                path.Base(filePath),
7✔
621
                DefaultImageGenerationLinkExpire,
7✔
622
        )
7✔
623
        if err != nil {
8✔
624
                return "", err
1✔
625
        }
1✔
626
        multipartMsg.GetArtifactURI = link.Uri
6✔
627

6✔
628
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
6✔
629
        if err != nil {
7✔
630
                return "", err
1✔
631
        }
1✔
632
        multipartMsg.DeleteArtifactURI = link.Uri
5✔
633

5✔
634
        return artifactID, nil
5✔
635
}
636

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

1✔
641
        image, err := d.db.FindImageByID(ctx, id)
1✔
642
        if err != nil {
1✔
643
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
644
        }
×
645

646
        if image == nil {
2✔
647
                return nil, nil
1✔
648
        }
1✔
649

650
        return image, nil
1✔
651
}
652

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

1✔
663
        if err != nil {
1✔
664
                return errors.Wrap(err, "Getting image metadata")
×
665
        }
×
666

667
        if found == nil {
1✔
668
                return ErrImageMetaNotFound
×
669
        }
×
670

671
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
672
        if err != nil {
1✔
673
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
674
        }
×
675

676
        // Image is in use, not allowed to delete
677
        if inUse {
2✔
678
                return ErrModelImageInActiveDeployment
1✔
679
        }
1✔
680

681
        // Delete image file (call to external service)
682
        // Noop for not existing file
683
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
684
        if err != nil {
1✔
685
                return err
×
686
        }
×
687
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
688
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
689
                return errors.Wrap(err, "Deleting image file")
×
690
        }
×
691

692
        // Delete metadata
693
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
694
                return errors.Wrap(err, "Deleting image metadata")
×
695
        }
×
696

697
        // update release
698
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
699
                return err
×
700
        }
×
701

702
        return nil
1✔
703
}
704

705
// ListImages according to specified filers.
706
func (d *Deployments) ListImages(
707
        ctx context.Context,
708
        filters *model.ReleaseOrImageFilter,
709
) ([]*model.Image, int, error) {
1✔
710
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
711
        if err != nil {
1✔
712
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
713
        }
×
714

715
        if imageList == nil {
2✔
716
                return make([]*model.Image, 0), 0, nil
1✔
717
        }
1✔
718

719
        return imageList, count, nil
1✔
720
}
721

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

×
726
        if err := constructor.Validate(); err != nil {
×
727
                return false, errors.Wrap(err, "Validating image metadata")
×
728
        }
×
729

730
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
731
        if err != nil {
×
732
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
733
        }
×
734

735
        if found {
×
736
                return false, ErrModelImageUsedInAnyDeployment
×
737
        }
×
738

739
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
740
        if err != nil {
×
741
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
742
        }
×
743

744
        if foundImage == nil {
×
745
                return false, nil
×
746
        }
×
747

748
        foundImage.SetModified(time.Now())
×
749
        foundImage.ImageMeta = constructor
×
750

×
751
        _, err = d.db.Update(ctx, foundImage)
×
752
        if err != nil {
×
753
                return false, errors.Wrap(err, "Updating image matadata")
×
754
        }
×
755

756
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
757
                return false, err
×
758
        }
×
759

760
        return true, nil
×
761
}
762

763
// DownloadLink presigned GET link to download image file.
764
// Returns error if image have not been uploaded.
765
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
766
        expire time.Duration) (*model.Link, error) {
1✔
767

1✔
768
        image, err := d.GetImage(ctx, imageID)
1✔
769
        if err != nil {
1✔
770
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
771
        }
×
772

773
        if image == nil {
1✔
774
                return nil, nil
×
775
        }
×
776

777
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
778
        if err != nil {
1✔
779
                return nil, err
×
780
        }
×
781
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
782
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
783
        if err != nil {
1✔
784
                return nil, errors.Wrap(err, "Searching for image file")
×
785
        }
×
786

787
        link, err := d.objectStorage.GetRequest(
1✔
788
                ctx,
1✔
789
                imagePath,
1✔
790
                image.Name+model.ArtifactFileSuffix,
1✔
791
                expire,
1✔
792
        )
1✔
793
        if err != nil {
1✔
794
                return nil, errors.Wrap(err, "Generating download link")
×
795
        }
×
796

797
        return link, nil
1✔
798
}
799

800
func (d *Deployments) UploadLink(
801
        ctx context.Context,
802
        expire time.Duration,
803
        skipVerify bool,
804
) (*model.UploadLink, error) {
6✔
805
        ctx, err := d.contextWithStorageSettings(ctx)
6✔
806
        if err != nil {
7✔
807
                return nil, err
1✔
808
        }
1✔
809

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

829
        return upLink, err
3✔
830
}
831

832
func (d *Deployments) processUploadedArtifact(
833
        ctx context.Context,
834
        artifactID string,
835
        artifact io.ReadCloser,
836
        skipVerify bool,
837
        metadata *model.DirectUploadMetadata,
838
) error {
5✔
839
        linkStatus := model.LinkStatusCompleted
5✔
840

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

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

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

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

950
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
951
        return &model.ArtifactInfo{
1✔
952
                Format:  info.Format,
1✔
953
                Version: uint(info.Version),
1✔
954
        }
1✔
955
}
1✔
956

957
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
958
        var files []model.UpdateFile
1✔
959
        for _, u := range uFiles {
2✔
960
                files = append(files, model.UpdateFile{
1✔
961
                        Name:     u.Name,
1✔
962
                        Size:     u.Size,
1✔
963
                        Date:     &u.Date,
1✔
964
                        Checksum: string(u.Checksum),
1✔
965
                })
1✔
966
        }
1✔
967
        return files, nil
1✔
968
}
969

970
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
5✔
971
        metaArtifact := model.NewArtifactMeta()
5✔
972

5✔
973
        aReader := areader.NewReader(*r)
5✔
974

5✔
975
        // There is no signature verification here.
5✔
976
        // It is just simple check if artifact is signed or not.
5✔
977
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
5✔
978
                metaArtifact.Signed = true
×
979
                return nil
×
980
        }
×
981

982
        var err error
5✔
983
        if skipVerify {
8✔
984
                err = aReader.ReadArtifactHeaders()
3✔
985
                if err != nil {
5✔
986
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
987
                }
2✔
988
        } else {
3✔
989
                err = aReader.ReadArtifact()
3✔
990
                if err != nil {
6✔
991
                        return nil, errors.Wrap(err, "reading artifact error")
3✔
992
                }
3✔
993
        }
994

995
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
996
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
997

1✔
998
        metaArtifact.Name = aReader.GetArtifactName()
1✔
999
        if metaArtifact.Info.Version == 3 {
2✔
1000
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
1001
                if err != nil {
1✔
1002
                        return nil, errors.Wrap(err,
×
1003
                                "error parsing version 3 artifact")
×
1004
                }
×
1005

1006
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
1007
                if err != nil {
1✔
1008
                        return nil, errors.Wrap(err,
×
1009
                                "error parsing version 3 artifact")
×
1010
                }
×
1011

1012
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
1013
        }
1014

1015
        for _, p := range aReader.GetHandlers() {
2✔
1016
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
1017
                if err != nil {
1✔
1018
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
1019
                }
×
1020

1021
                uMetadata, err := p.GetUpdateMetaData()
1✔
1022
                if err != nil {
1✔
1023
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
1024
                }
×
1025

1026
                metaArtifact.Updates = append(
1✔
1027
                        metaArtifact.Updates,
1✔
1028
                        model.Update{
1✔
1029
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
1030
                                        Type: p.GetUpdateType(),
1✔
1031
                                },
1✔
1032
                                Files:    uFiles,
1✔
1033
                                MetaData: uMetadata,
1✔
1034
                        })
1✔
1035
        }
1036

1037
        return metaArtifact, nil
1✔
1038
}
1039

1040
func getArtifactIDs(artifacts []*model.Image) []string {
7✔
1041
        artifactIDs := make([]string, 0, len(artifacts))
7✔
1042
        for _, artifact := range artifacts {
14✔
1043
                artifactIDs = append(artifactIDs, artifact.Id)
7✔
1044
        }
7✔
1045
        return artifactIDs
7✔
1046
}
1047

1048
// deployments
1049
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
4✔
1050
        ids := make([]string, len(devices))
4✔
1051
        for i, d := range devices {
8✔
1052
                ids[i] = d.ID
4✔
1053
        }
4✔
1054

1055
        return ids
4✔
1056
}
1057

1058
// updateDeploymentConstructor fills devices list with device ids
1059
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
1060
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
5✔
1061
        l := log.FromContext(ctx)
5✔
1062

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

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

1111
        return constructor, nil
3✔
1112
}
1113

1114
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1115
func (d *Deployments) CreateDeviceConfigurationDeployment(
1116
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1117
        deviceID, deploymentID string) (string, error) {
5✔
1118

5✔
1119
        if constructor == nil {
6✔
1120
                return "", ErrModelMissingInput
1✔
1121
        }
1✔
1122

1123
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
4✔
1124
                constructor,
4✔
1125
                deploymentID,
4✔
1126
        )
4✔
1127
        if err != nil {
4✔
1128
                return "", errors.Wrap(err, "failed to create deployment")
×
1129
        }
×
1130

1131
        deployment.DeviceList = []string{deviceID}
4✔
1132
        deployment.MaxDevices = 1
4✔
1133
        deployment.Configuration = []byte(constructor.Configuration)
4✔
1134
        deployment.Type = model.DeploymentTypeConfiguration
4✔
1135

4✔
1136
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
4✔
1137
        if err != nil {
5✔
1138
                return "", err
1✔
1139
        }
1✔
1140
        deployment.Groups = groups
3✔
1141

3✔
1142
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
5✔
1143
                if strings.Contains(err.Error(), "duplicate key error") {
3✔
1144
                        return "", ErrDuplicateDeployment
1✔
1145
                }
1✔
1146
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
3✔
1147
                        return "", ErrInvalidDeploymentID
1✔
1148
                }
1✔
1149
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1150
        }
1151

1152
        return deployment.Id, nil
2✔
1153
}
1154

1155
// CreateDeployment precomputes new deployment and schedules it for devices.
1156
func (d *Deployments) CreateDeployment(ctx context.Context,
1157
        constructor *model.DeploymentConstructor) (string, error) {
9✔
1158

9✔
1159
        var err error
9✔
1160

9✔
1161
        if constructor == nil {
10✔
1162
                return "", ErrModelMissingInput
1✔
1163
        }
1✔
1164

1165
        if err := constructor.Validate(); err != nil {
8✔
1166
                return "", errors.Wrap(err, "Validating deployment")
×
1167
        }
×
1168

1169
        if len(constructor.Group) > 0 || constructor.AllDevices {
13✔
1170
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
5✔
1171
                if err != nil {
7✔
1172
                        return "", err
2✔
1173
                }
2✔
1174
        }
1175

1176
        deployment, err := model.NewDeploymentFromConstructor(constructor)
6✔
1177
        if err != nil {
6✔
1178
                return "", errors.Wrap(err, "failed to create deployment")
×
1179
        }
×
1180

1181
        // Assign artifacts to the deployment.
1182
        // When new artifact(s) with the artifact name same as the one in the deployment
1183
        // will be uploaded to the backend, it will also become part of this deployment.
1184
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
6✔
1185
        if err != nil {
6✔
1186
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1187
        }
×
1188

1189
        if len(artifacts) == 0 {
7✔
1190
                return "", ErrNoArtifact
1✔
1191
        }
1✔
1192

1193
        deployment.Artifacts = getArtifactIDs(artifacts)
6✔
1194
        deployment.DeviceList = constructor.Devices
6✔
1195
        deployment.MaxDevices = len(constructor.Devices)
6✔
1196
        deployment.Type = model.DeploymentTypeSoftware
6✔
1197
        if len(constructor.Group) > 0 {
9✔
1198
                deployment.Groups = []string{constructor.Group}
3✔
1199
        }
3✔
1200

1201
        // single device deployment case
1202
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
9✔
1203
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
3✔
1204
                if err != nil {
3✔
1205
                        return "", err
×
1206
                }
×
1207
                deployment.Groups = groups
3✔
1208
        }
1209

1210
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
7✔
1211
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1212
        }
1✔
1213

1214
        return deployment.Id, nil
5✔
1215
}
1216

1217
func (d *Deployments) getDeploymentGroups(
1218
        ctx context.Context,
1219
        devices []string,
1220
) ([]string, error) {
6✔
1221
        id := identity.FromContext(ctx)
6✔
1222

6✔
1223
        //only for single device deployment case
6✔
1224
        if len(devices) != 1 {
6✔
1225
                return nil, nil
×
1226
        }
×
1227

1228
        if id == nil {
7✔
1229
                id = &identity.Identity{}
1✔
1230
        }
1✔
1231

1232
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
6✔
1233
        if err != nil && err != inventory.ErrDevNotFound {
7✔
1234
                return nil, err
1✔
1235
        }
1✔
1236
        return groups, nil
5✔
1237
}
1238

1239
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1240
func (d *Deployments) IsDeploymentFinished(
1241
        ctx context.Context,
1242
        deploymentID string,
1243
) (bool, error) {
1✔
1244
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1245
        if err != nil {
1✔
1246
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1247
        }
×
1248
        if deployment == nil {
2✔
1249
                return true, nil
1✔
1250
        }
1✔
1251

1252
        return false, nil
1✔
1253
}
1254

1255
// GetDeployment fetches deployment by ID
1256
func (d *Deployments) GetDeployment(ctx context.Context,
1257
        deploymentID string) (*model.Deployment, error) {
1✔
1258

1✔
1259
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1260
        if err != nil {
1✔
1261
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1262
        }
×
1263

1264
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1265
                return nil, err
×
1266
        }
×
1267

1268
        return deployment, nil
1✔
1269
}
1270

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

4✔
1276
        var found bool
4✔
1277

4✔
1278
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
4✔
1279
        if err != nil {
5✔
1280
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1281
        }
1✔
1282

1283
        if found {
4✔
1284
                return found, nil
1✔
1285
        }
1✔
1286

1287
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
3✔
1288
                imageID, model.ActiveDeploymentStatuses()...)
3✔
1289
        if err != nil {
4✔
1290
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1291
        }
1✔
1292

1293
        return found, nil
2✔
1294
}
1295

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

×
1300
        var found bool
×
1301

×
1302
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1303
        if err != nil {
×
1304
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1305
        }
×
1306

1307
        if found {
×
1308
                return found, nil
×
1309
        }
×
1310

1311
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
1312
        if err != nil {
×
1313
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
1314
        }
×
1315

1316
        return found, nil
×
1317
}
1318

1319
// Retrieves the model.Deployment and model.DeviceDeployment structures
1320
// for the device. Upon error, nil is returned for both deployment structures.
1321
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1322
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1323

2✔
1324
        // Retrieve device deployment
2✔
1325
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
2✔
1326

2✔
1327
        if err != nil {
2✔
1328
                return nil, nil, errors.Wrap(err,
×
1329
                        "Searching for oldest active deployment for the device")
×
1330
        } else if deviceDeployment == nil {
3✔
1331
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1332
        }
1✔
1333

1334
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1335
        if err != nil {
2✔
1336
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1337
        }
×
1338
        if deployment == nil {
2✔
1339
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1340
        }
×
1341

1342
        return deployment, deviceDeployment, nil
2✔
1343
}
1344

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

1✔
1355
        var lastDeployment *time.Time
1✔
1356
        //get latest device deployment for the device;
1✔
1357
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1358
        if err != nil {
1✔
1359
                return nil, nil, errors.Wrap(err,
×
1360
                        "Searching for latest active deployment for the device")
×
1361
        } else if deviceDeployment == nil {
2✔
1362
                lastDeployment = &time.Time{}
1✔
1363
        } else {
2✔
1364
                lastDeployment = deviceDeployment.Created
1✔
1365
        }
1✔
1366

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

1379
                for _, deployment := range deployments {
2✔
1380
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1381
                        if err != nil {
1✔
1382
                                return nil, nil, err
×
1383
                        }
×
1384
                        if ok {
2✔
1385
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1386
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1387
                                if err != nil {
1✔
1388
                                        return nil, nil, err
×
1389
                                }
×
1390
                                return deployment, deviceDeployment, nil
1✔
1391
                        }
1392
                }
1393
        }
1394

1395
        return nil, nil, nil
×
1396
}
1397

1398
func (d *Deployments) createDeviceDeploymentWithStatus(
1399
        ctx context.Context, deviceID string,
1400
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1401
) (*model.DeviceDeployment, error) {
6✔
1402
        prevStatus := model.DeviceDeploymentStatusNull
6✔
1403
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
6✔
1404
        if err != nil && err != mongo.ErrStorageNotFound {
6✔
1405
                return nil, err
×
1406
        } else if deviceDeployment != nil {
6✔
1407
                prevStatus = deviceDeployment.Status
×
1408
        }
×
1409

1410
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
6✔
1411
        deviceDeployment.Status = status
6✔
1412
        deviceDeployment.Active = status.Active()
6✔
1413
        deviceDeployment.Created = deployment.Created
6✔
1414

6✔
1415
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
6✔
1416
                return nil, err
×
1417
        }
×
1418

1419
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
6✔
1420
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
6✔
1421
                return nil, err
×
1422
        }
×
1423

1424
        // after inserting new device deployment update deployment stats
1425
        // in the database and locally, and update deployment status
1426
        if err := d.db.UpdateStatsInc(
6✔
1427
                ctx, deployment.Id,
6✔
1428
                prevStatus, status,
6✔
1429
        ); err != nil {
6✔
1430
                return nil, err
×
1431
        }
×
1432

1433
        deployment.Stats.Inc(status)
6✔
1434

6✔
1435
        err = d.recalcDeploymentStatus(ctx, deployment)
6✔
1436
        if err != nil {
6✔
1437
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1438
        }
×
1439

1440
        if !status.Active() {
11✔
1441
                err := d.reindexDevice(ctx, deviceID)
5✔
1442
                if err != nil {
5✔
1443
                        l := log.FromContext(ctx)
×
1444
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1445
                }
×
1446
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1447
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1448
                        l := log.FromContext(ctx)
×
1449
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1450
                }
×
1451
        }
1452

1453
        return deviceDeployment, nil
6✔
1454
}
1455

1456
func (d *Deployments) isDevicePartOfDeployment(
1457
        ctx context.Context,
1458
        deviceID string,
1459
        deployment *model.Deployment,
1460
) (bool, error) {
8✔
1461
        for _, id := range deployment.DeviceList {
14✔
1462
                if id == deviceID {
12✔
1463
                        return true, nil
6✔
1464
                }
6✔
1465
        }
1466
        return false, nil
3✔
1467
}
1468

1469
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1470
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1471
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
2✔
1472

2✔
1473
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
2✔
1474
        if err != nil {
2✔
1475
                return nil, ErrModelInternal
×
1476
        } else if deployment == nil {
3✔
1477
                return nil, nil
1✔
1478
        }
1✔
1479

1480
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
2✔
1481
        if err != nil {
3✔
1482
                return nil, err
1✔
1483
        }
1✔
1484
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
2✔
1485
}
1486

1487
func (d *Deployments) getDeploymentInstructions(
1488
        ctx context.Context,
1489
        deployment *model.Deployment,
1490
        deviceDeployment *model.DeviceDeployment,
1491
        request *model.DeploymentNextRequest,
1492
) (*model.DeploymentInstructions, error) {
2✔
1493

2✔
1494
        var newArtifactAssigned bool
2✔
1495

2✔
1496
        l := log.FromContext(ctx)
2✔
1497

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

1514
        // assing artifact to the device deployment
1515
        // only if it was not assgined previously
1516
        if deviceDeployment.Image == nil {
4✔
1517
                if err := d.assignArtifact(
2✔
1518
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
2✔
1519
                        return nil, err
×
1520
                }
×
1521
                newArtifactAssigned = true
2✔
1522
        }
1523

1524
        if deviceDeployment.Image == nil {
2✔
1525
                // No artifact - return empty response
×
1526
                return nil, nil
×
1527
        }
×
1528

1529
        // if the deployment is not forcing the installation, and
1530
        // if artifact was recognized as already installed, and this is
1531
        // a new device deployment - indicated by device deployment status "pending",
1532
        // handle already installed artifact case
1533
        if !deployment.ForceInstallation &&
2✔
1534
                d.isAlreadyInstalled(request, deviceDeployment) &&
2✔
1535
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
4✔
1536
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
2✔
1537
        }
2✔
1538

1539
        // if new artifact has been assigned to device deployment
1540
        // add artifact size to deployment total size,
1541
        // before returning deployment instruction to the device
1542
        if newArtifactAssigned {
2✔
1543
                if err := d.db.IncrementDeploymentTotalSize(
1✔
1544
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
1545
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1546
                }
×
1547
        }
1548

1549
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1550
        if err != nil {
1✔
1551
                return nil, err
×
1552
        }
×
1553

1554
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1555
        link, err := d.objectStorage.GetRequest(
1✔
1556
                ctx,
1✔
1557
                imagePath,
1✔
1558
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1559
                DefaultUpdateDownloadLinkExpire,
1✔
1560
        )
1✔
1561
        if err != nil {
1✔
1562
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1563
        }
×
1564

1565
        instructions := &model.DeploymentInstructions{
1✔
1566
                ID: deviceDeployment.DeploymentId,
1✔
1567
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1568
                        ID: deviceDeployment.Image.Id,
1✔
1569
                        ArtifactName: deviceDeployment.Image.
1✔
1570
                                ArtifactMeta.Name,
1✔
1571
                        Source: *link,
1✔
1572
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1573
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1574
                },
1✔
1575
        }
1✔
1576

1✔
1577
        return instructions, nil
1✔
1578
}
1579

1580
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1581
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
2✔
1582
        if deviceDeployment.Request != nil {
3✔
1583
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1584
                        // the device reported different device type and/or artifact name during the
1✔
1585
                        // update process, this can happen if the mender-store DB in the client is not
1✔
1586
                        // persistent so a new deployment start without a previous one is still ongoing;
1✔
1587
                        // mark deployment for this device as failed to force client to rollback
1✔
1588
                        l := log.FromContext(ctx)
1✔
1589
                        l.Errorf(
1✔
1590
                                "Device with id %s reported new data: %s during update process;"+
1✔
1591
                                        "old data: %s",
1✔
1592
                                deviceID, request, deviceDeployment.Request)
1✔
1593

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

1620
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1621
// ID `deviceID`. Returns nil if update was successful.
1622
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1623
        deviceID string, ddState model.DeviceDeploymentState) error {
6✔
1624

6✔
1625
        l := log.FromContext(ctx)
6✔
1626

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

6✔
1629
        var finishTime *time.Time = nil
6✔
1630
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
10✔
1631
                now := time.Now()
4✔
1632
                finishTime = &now
4✔
1633
        }
4✔
1634

1635
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
6✔
1636
        if err == mongo.ErrStorageNotFound {
7✔
1637
                return ErrStorageNotFound
1✔
1638
        } else if err != nil {
6✔
1639
                return err
×
1640
        }
×
1641

1642
        currentStatus := dd.Status
5✔
1643

5✔
1644
        if currentStatus == model.DeviceDeploymentStatusAborted {
5✔
1645
                return ErrDeploymentAborted
×
1646
        }
×
1647

1648
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
5✔
1649
                return ErrDeviceDecommissioned
×
1650
        }
×
1651

1652
        // nothing to do
1653
        if ddState.Status == currentStatus {
5✔
1654
                return nil
×
1655
        }
×
1656

1657
        // update finish time
1658
        ddState.FinishTime = finishTime
5✔
1659

5✔
1660
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
5✔
1661
                deviceID, deploymentID, ddState)
5✔
1662
        if err != nil {
5✔
1663
                return err
×
1664
        }
×
1665

1666
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
5✔
1667
                return err
×
1668
        }
×
1669

1670
        // fetch deployment stats and update deployment status
1671
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
1672
        if err != nil {
5✔
1673
                return errors.Wrap(err, "failed when searching for deployment")
×
1674
        }
×
1675

1676
        err = d.recalcDeploymentStatus(ctx, deployment)
5✔
1677
        if err != nil {
5✔
1678
                return errors.Wrap(err, "failed to update deployment status")
×
1679
        }
×
1680

1681
        if !ddState.Status.Active() {
9✔
1682
                l := log.FromContext(ctx)
4✔
1683
                ldd := model.DeviceDeployment{
4✔
1684
                        DeviceId:     dd.DeviceId,
4✔
1685
                        DeploymentId: dd.DeploymentId,
4✔
1686
                        Id:           dd.Id,
4✔
1687
                        Status:       ddState.Status,
4✔
1688
                }
4✔
1689
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
4✔
1690
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1691
                }
×
1692
                if err := d.reindexDevice(ctx, deviceID); err != nil {
4✔
1693
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1694
                }
×
1695
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
4✔
1696
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1697
                }
×
1698
        }
1699

1700
        return nil
5✔
1701
}
1702

1703
// recalcDeploymentStatus inspects the deployment stats and
1704
// recalculates and updates its status
1705
// it should be used whenever deployment stats are touched
1706
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
10✔
1707
        status := dep.GetStatus()
10✔
1708

10✔
1709
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
10✔
1710
                return err
×
1711
        }
×
1712

1713
        return nil
10✔
1714
}
1715

1716
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1717
        deploymentID string) (model.Stats, error) {
1✔
1718

1✔
1719
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1720

1✔
1721
        if err != nil {
1✔
1722
                return nil, errors.Wrap(err, "checking deployment id")
×
1723
        }
×
1724

1725
        if deployment == nil {
1✔
1726
                return nil, nil
×
1727
        }
×
1728

1729
        return deployment.Stats, nil
1✔
1730
}
1731
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1732
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1733

×
1734
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1735

×
1736
        if err != nil {
×
1737
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1738
        }
×
1739

1740
        if deploymentStats == nil {
×
1741
                return nil, ErrModelDeploymentNotFound
×
1742
        }
×
1743

1744
        return deploymentStats, nil
×
1745
}
1746

1747
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1748
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1749
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1750

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

1756
        if deployment == nil {
1✔
1757
                return nil, ErrModelDeploymentNotFound
×
1758
        }
×
1759

1760
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1761
        if err != nil {
1✔
1762
                return nil, ErrModelInternal
×
1763
        }
×
1764

1765
        return statuses, nil
1✔
1766
}
1767

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

1✔
1771
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1772
        if err != nil {
1✔
1773
                return nil, -1, ErrModelInternal
×
1774
        }
×
1775

1776
        if deployment == nil {
1✔
1777
                return nil, -1, ErrModelDeploymentNotFound
×
1778
        }
×
1779

1780
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1781
        if err != nil {
1✔
1782
                return nil, -1, ErrModelInternal
×
1783
        }
×
1784

1785
        return statuses, totalCount, nil
1✔
1786
}
1787

1788
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1789
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
4✔
1790
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
4✔
1791
        if err != nil {
5✔
1792
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1793
        }
1✔
1794

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

1811
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1812
        for _, deployment := range deployments {
5✔
1813
                deploymentsMap[deployment.Id] = deployment
3✔
1814
        }
3✔
1815

1816
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1817
        for i, deviceDeployment := range deviceDeployments {
6✔
1818
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
7✔
1819
                        res = append(res, model.DeviceDeploymentListItem{
3✔
1820
                                Id:         deviceDeployment.Id,
3✔
1821
                                Deployment: deployment,
3✔
1822
                                Device:     &deviceDeployments[i],
3✔
1823
                        })
3✔
1824
                } else {
4✔
1825
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1826
                                Id:     deviceDeployment.Id,
1✔
1827
                                Device: &deviceDeployments[i],
1✔
1828
                        })
1✔
1829
                }
1✔
1830
        }
1831

1832
        return res, totalCount, nil
2✔
1833
}
1834

1835
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1836
        ctx context.Context,
1837
        deployment *model.Deployment,
1838
) error {
6✔
1839
        if deployment.DeviceCount == nil {
6✔
1840
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1841
                if err != nil {
×
1842
                        return errors.Wrap(err, "counting device deployments")
×
1843
                }
×
1844
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1845
                if err != nil {
×
1846
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1847
                }
×
1848
                deployment.DeviceCount = &deviceCount
×
1849
        }
1850

1851
        return nil
6✔
1852
}
1853

1854
func (d *Deployments) LookupDeployment(ctx context.Context,
1855
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1856
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1857

1✔
1858
        if err != nil {
1✔
1859
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1860
        }
×
1861

1862
        if list == nil {
2✔
1863
                return make([]*model.Deployment, 0), 0, nil
1✔
1864
        }
1✔
1865

1866
        for _, deployment := range list {
×
1867
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1868
                        return nil, 0, err
×
1869
                }
×
1870
        }
1871

1872
        return list, totalCount, nil
×
1873
}
1874

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

1✔
1880
        // repack to temporary deployment log and validate
1✔
1881
        dlog := model.DeploymentLog{
1✔
1882
                DeviceID:     deviceID,
1✔
1883
                DeploymentID: deploymentID,
1✔
1884
                Messages:     logs,
1✔
1885
        }
1✔
1886
        if err := dlog.Validate(); err != nil {
1✔
1887
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1888
        }
×
1889

1890
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1891
                if err != nil {
×
1892
                        return err
×
1893
                } else {
×
1894
                        return ErrModelDeploymentNotFound
×
1895
                }
×
1896
        }
1897

1898
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1899
                return err
×
1900
        }
×
1901

1902
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1903
                deviceID, deploymentID, true)
1✔
1904
}
1905

1906
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1907
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1908

1✔
1909
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1910
                deviceID, deploymentID)
1✔
1911
}
1✔
1912

1913
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1914
        deploymentID string, deviceID string) (bool, error) {
1✔
1915
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1916
}
1✔
1917

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

5✔
1921
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
6✔
1922
                return err
1✔
1923
        }
1✔
1924

1925
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
4✔
1926
                ctx, deploymentID)
4✔
1927
        if err != nil {
5✔
1928
                return err
1✔
1929
        }
1✔
1930

1931
        // update statistics
1932
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
4✔
1933
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1934
        }
1✔
1935

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

1946
        return nil
2✔
1947
}
1948

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

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

2015
        if err := d.reindexDevice(ctx, deviceId); err != nil {
13✔
2016
                l := log.FromContext(ctx)
×
2017
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
2018
        }
×
2019

2020
        return nil
13✔
2021
}
2022

2023
// DecommissionDevice updates the status of all the pending and active deployments for a device
2024
// to decommissioned
2025
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
7✔
2026
        return d.updateDeviceDeploymentsStatus(
7✔
2027
                ctx,
7✔
2028
                deviceId,
7✔
2029
                model.DeviceDeploymentStatusDecommissioned,
7✔
2030
        )
7✔
2031
}
7✔
2032

2033
// AbortDeviceDeployments aborts all the pending and active deployments for a device
2034
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
8✔
2035
        return d.updateDeviceDeploymentsStatus(
8✔
2036
                ctx,
8✔
2037
                deviceId,
8✔
2038
                model.DeviceDeploymentStatusAborted,
8✔
2039
        )
8✔
2040
}
8✔
2041

2042
// DeleteDeviceDeploymentsHistory deletes the device deployments history
2043
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
2✔
2044
        // get device deployments which will be marked as deleted
2✔
2045
        f := false
2✔
2046
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
2✔
2047
        if err != nil {
2✔
2048
                return err
×
2049
        }
×
2050

2051
        // no device deployments to update
2052
        if len(dd) <= 0 {
2✔
2053
                return nil
×
2054
        }
×
2055

2056
        // mark device deployments as deleted
2057
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
3✔
2058
                return err
1✔
2059
        }
1✔
2060

2061
        // trigger reindexing of updated device deployments
2062
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2063
        for i, d := range dd {
2✔
2064
                deviceDeployments[i].ID = d.Id
1✔
2065
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2066
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2067
        }
1✔
2068
        if d.reportingClient != nil {
2✔
2069
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2070
        }
1✔
2071
        return err
1✔
2072
}
2073

2074
// Storage settings
2075
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
2076
        settings, err := d.db.GetStorageSettings(ctx)
3✔
2077
        if err != nil {
4✔
2078
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2079
        }
1✔
2080

2081
        return settings, nil
2✔
2082
}
2083

2084
func (d *Deployments) SetStorageSettings(
2085
        ctx context.Context,
2086
        storageSettings *model.StorageSettings,
2087
) error {
4✔
2088
        if storageSettings != nil {
8✔
2089
                ctx = storage.SettingsWithContext(ctx, storageSettings)
4✔
2090
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
4✔
2091
                        return errors.WithMessage(err,
×
2092
                                "the provided storage settings failed the health check",
×
2093
                        )
×
2094
                }
×
2095
        }
2096
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
6✔
2097
                return errors.Wrap(err, "Failed to save settings")
2✔
2098
        }
2✔
2099

2100
        return nil
2✔
2101
}
2102

2103
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
8✔
2104
        d.reportingClient = c
8✔
2105
        return d
8✔
2106
}
8✔
2107

2108
func (d *Deployments) haveReporting() bool {
6✔
2109
        return d.reportingClient != nil
6✔
2110
}
6✔
2111

2112
func (d *Deployments) search(
2113
        ctx context.Context,
2114
        tid string,
2115
        parms model.SearchParams,
2116
) ([]model.InvDevice, int, error) {
6✔
2117
        if d.haveReporting() {
7✔
2118
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2119
        } else {
6✔
2120
                return d.inventoryClient.Search(ctx, tid, parms)
5✔
2121
        }
5✔
2122
}
2123

2124
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2125
        ctx context.Context,
2126
        artifactName string,
2127
) error {
2✔
2128
        // first check if there are pending deployments with given artifact name
2✔
2129
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
2✔
2130
        if err != nil {
2✔
2131
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
2132
        }
×
2133
        if !exists {
3✔
2134
                return nil
1✔
2135
        }
1✔
2136

2137
        // Assign artifacts to the deployments with given artifact name
2138
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2139
        if err != nil {
1✔
2140
                return errors.Wrap(err, "Finding artifact with given name")
×
2141
        }
×
2142

2143
        if len(artifacts) == 0 {
1✔
2144
                return ErrNoArtifact
×
2145
        }
×
2146
        artifactIDs := getArtifactIDs(artifacts)
1✔
2147
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2148
}
2149

2150
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
25✔
2151
        if d.reportingClient != nil {
28✔
2152
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
3✔
2153
        }
3✔
2154
        return nil
22✔
2155
}
2156

2157
func (d *Deployments) reindexDeployment(ctx context.Context,
2158
        deviceID, deploymentID, ID string) error {
17✔
2159
        if d.reportingClient != nil {
20✔
2160
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
3✔
2161
        }
3✔
2162
        return nil
14✔
2163
}
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