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

mendersoftware / mender-server / 1925715791

14 Jul 2025 02:01PM UTC coverage: 65.487% (-0.02%) from 65.504%
1925715791

Pull #790

gitlab-ci

bahaa-ghazal
feat(deployments): Implement new v2 GET `/artifacts` endpoint

Ticket: MEN-8181
Changelog: Title
Signed-off-by: Bahaa Aldeen Ghazal <bahaa.ghazal@northern.tech>
Pull Request #790: feat(deployments): Implement new v2 GET `/artifacts` endpoint

145 of 237 new or added lines in 7 files covered. (61.18%)

129 existing lines in 3 files now uncovered.

32534 of 49680 relevant lines covered (65.49%)

1.38 hits per line

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

78.76
/backend/services/deployments/app/app.go
1
// Copyright 2024 Northern.tech AS
2
//
3
//        Licensed under the Apache License, Version 2.0 (the "License");
4
//        you may not use this file except in compliance with the License.
5
//        You may obtain a copy of the License at
6
//
7
//            http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//        Unless required by applicable law or agreed to in writing, software
10
//        distributed under the License is distributed on an "AS IS" BASIS,
11
//        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//        See the License for the specific language governing permissions and
13
//        limitations under the License.
14

15
package 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/mender-artifact/areader"
31
        "github.com/mendersoftware/mender-artifact/artifact"
32
        "github.com/mendersoftware/mender-artifact/awriter"
33
        "github.com/mendersoftware/mender-artifact/handlers"
34

35
        "github.com/mendersoftware/mender-server/pkg/identity"
36
        "github.com/mendersoftware/mender-server/pkg/log"
37

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

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

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

63
        fileSuffixTmp = ".tmp"
64

65
        inprogressIdleTime = time.Hour
66
)
67

68
var (
69
        ArtifactConfigureType = "mender-configure"
70
)
71

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

89
        ErrMsgArtifactConflict = "An artifact with the same name has conflicting dependencies"
90

91
        // deployments
92
        ErrModelMissingInput       = errors.New("Missing input deployment data")
93
        ErrModelInvalidDeviceID    = errors.New("Invalid device ID")
94
        ErrModelDeploymentNotFound = errors.New("Deployment not found")
95
        ErrModelInternal           = errors.New("Internal error")
96
        ErrStorageInvalidLog       = errors.New("Invalid deployment log")
97
        ErrStorageNotFound         = errors.New("Not found")
98
        ErrDeploymentAborted       = errors.New("Deployment aborted")
99
        ErrDeviceDecommissioned    = errors.New("Device decommissioned")
100
        ErrNoArtifact              = errors.New("No artifact for the deployment")
101
        ErrNoDevices               = errors.New("No devices for the deployment")
102
        ErrDuplicateDeployment     = errors.New("Deployment with given ID already exists")
103
        ErrInvalidDeploymentID     = errors.New("Deployment ID must be a valid UUID")
104
        ErrConflictingRequestData  = errors.New("Device provided conflicting request data")
105
        ErrConflictingDeployment   = errors.New(
106
                "Invalid deployment definition: there is already an active deployment with " +
107
                        "the same parameters",
108
        )
109
)
110

111
//deployments
112

113
//go:generate ../../../utils/mockgen.sh
114
type App interface {
115
        HealthCheck(ctx context.Context) error
116
        // limits
117
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
118
        ProvisionTenant(ctx context.Context, tenant_id string) error
119

120
        // Storage Settings
121
        GetStorageSettings(ctx context.Context) (*model.StorageSettings, error)
122
        SetStorageSettings(ctx context.Context, storageSettings *model.StorageSettings) error
123

124
        // images
125
        ListImages(
126
                ctx context.Context,
127
                filters *model.ReleaseOrImageFilter,
128
        ) ([]*model.Image, int, error)
129
        ListImagesV2(
130
                ctx context.Context,
131
                filters *model.ImageFilter,
132
        ) ([]*model.Image, error)
133
        DownloadLink(ctx context.Context, imageID string,
134
                expire time.Duration) (*model.Link, error)
135
        UploadLink(
136
                ctx context.Context,
137
                expire time.Duration,
138
                skipVerify bool,
139
        ) (*model.UploadLink, error)
140
        CompleteUpload(
141
                ctx context.Context,
142
                intentID string,
143
                skipVerify bool,
144
                metadata *model.DirectUploadMetadata,
145
        ) error
146
        GetImage(ctx context.Context, id string) (*model.Image, error)
147
        DeleteImage(ctx context.Context, imageID string) error
148
        CreateImage(ctx context.Context,
149
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
150
        GenerateImage(ctx context.Context,
151
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
152
        GenerateConfigurationImage(
153
                ctx context.Context,
154
                deviceType string,
155
                deploymentID string,
156
        ) (io.Reader, error)
157
        EditImage(ctx context.Context, id string,
158
                constructorData *model.ImageMeta) (bool, error)
159

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

205
        // releases
206
        ReplaceReleaseTags(ctx context.Context, releaseName string, tags model.Tags) error
207
        UpdateRelease(ctx context.Context, releaseName string, release model.ReleasePatch) error
208
        ListReleaseTags(ctx context.Context) (model.Tags, error)
209
        GetReleasesUpdateTypes(ctx context.Context) ([]string, error)
210
        DeleteReleases(ctx context.Context, releaseNames []string) ([]string, error)
211
        GetRelease(ctx context.Context, releaseName string) (*model.Release, error)
212
}
213

214
type Deployments struct {
215
        db              store.DataStore
216
        objectStorage   storage.ObjectStorage
217
        workflowsClient workflows.Client
218
        inventoryClient inventory.Client
219
        reportingClient reporting.Client
220
}
221

222
// Compile-time check
223
var _ App = &Deployments{}
224

225
func NewDeployments(
226
        storage store.DataStore,
227
        objectStorage storage.ObjectStorage,
228
        maxActiveDeployments int64,
229
        withAuditLogs bool,
230
) *Deployments {
3✔
231
        return &Deployments{
3✔
232
                db:              storage,
3✔
233
                objectStorage:   objectStorage,
3✔
234
                workflowsClient: workflows.NewClient(),
3✔
235
                inventoryClient: inventory.NewClient(),
3✔
236
        }
3✔
237
}
3✔
238

239
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
1✔
240
        d.workflowsClient = workflowsClient
1✔
241
}
1✔
242

243
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
1✔
244
        d.inventoryClient = inventoryClient
1✔
245
}
1✔
246

247
func (d *Deployments) HealthCheck(ctx context.Context) error {
2✔
248
        err := d.db.Ping(ctx)
2✔
249
        if err != nil {
3✔
250
                return errors.Wrap(err, "error reaching MongoDB")
1✔
251
        }
1✔
252
        err = d.objectStorage.HealthCheck(ctx)
2✔
253
        if err != nil {
3✔
254
                return errors.Wrap(
1✔
255
                        err,
1✔
256
                        "error reaching artifact storage service",
1✔
257
                )
1✔
258
        }
1✔
259

260
        err = d.workflowsClient.CheckHealth(ctx)
2✔
261
        if err != nil {
3✔
262
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
263
        }
1✔
264

265
        err = d.inventoryClient.CheckHealth(ctx)
2✔
266
        if err != nil {
3✔
267
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
268
        }
1✔
269

270
        if d.reportingClient != nil {
3✔
271
                err = d.reportingClient.CheckHealth(ctx)
1✔
272
                if err != nil {
2✔
273
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
274
                }
1✔
275
        }
276
        return nil
2✔
277
}
278

279
func (d *Deployments) contextWithStorageSettings(
280
        ctx context.Context,
281
) (context.Context, error) {
3✔
282
        var err error
3✔
283
        settings, ok := storage.SettingsFromContext(ctx)
3✔
284
        if !ok {
6✔
285
                settings, err = d.db.GetStorageSettings(ctx)
3✔
286
                if err != nil {
4✔
287
                        return nil, err
1✔
288
                }
1✔
289
        }
290
        if settings != nil {
3✔
291
                if settings.UseAccelerate && settings.Uri != "" {
×
292
                        log.FromContext(ctx).
×
293
                                Warn(`storage settings: custom "uri" and "use_accelerate" ` +
×
294
                                        `are not allowed: disabling transfer acceleration`)
×
295
                        settings.UseAccelerate = false
×
296
                }
×
297
                err = settings.Validate()
×
298
                if err != nil {
×
299
                        return nil, err
×
300
                }
×
301
        }
302
        return storage.SettingsWithContext(ctx, settings), nil
3✔
303
}
304

305
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
1✔
306
        limit, err := d.db.GetLimit(ctx, name)
1✔
307
        if err == mongo.ErrLimitNotFound {
2✔
308
                return &model.Limit{
1✔
309
                        Name:  name,
1✔
310
                        Value: 0,
1✔
311
                }, nil
1✔
312

1✔
313
        } else if err != nil {
3✔
314
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
315
        }
1✔
316
        return limit, nil
1✔
317
}
318

319
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
2✔
320
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
3✔
321
                return errors.Wrap(err, "failed to provision tenant")
1✔
322
        }
1✔
323

324
        return nil
2✔
325
}
326

327
// CreateImage parses artifact and uploads artifact file to the file storage - in parallel,
328
// and creates image structure in the system.
329
// Returns image ID and nil on success.
330
func (d *Deployments) CreateImage(ctx context.Context,
331
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
2✔
332
        return d.handleArtifact(ctx, multipartUploadMsg, false, nil)
2✔
333
}
2✔
334

335
func (d *Deployments) saveUpdateTypes(ctx context.Context, image *model.Image) {
2✔
336
        l := log.FromContext(ctx)
2✔
337
        if image != nil && image.ArtifactMeta != nil && len(image.ArtifactMeta.Updates) > 0 {
4✔
338
                i := 0
2✔
339
                updateTypes := make([]string, len(image.ArtifactMeta.Updates))
2✔
340
                for _, t := range image.ArtifactMeta.Updates {
4✔
341
                        if t.TypeInfo.Type == nil {
3✔
342
                                continue
1✔
343
                        }
344
                        updateTypes[i] = *t.TypeInfo.Type
2✔
345
                        i++
2✔
346
                }
347
                err := d.db.SaveUpdateTypes(ctx, updateTypes[:i])
2✔
348
                if err != nil {
2✔
349
                        l.Errorf(
×
350
                                "error while saving the update types for the artifact: %s",
×
351
                                err.Error(),
×
352
                        )
×
353
                }
×
354
        }
355
}
356

357
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
358
// and creates image structure in the system.
359
// Returns image ID, artifact file ID and nil on success.
360
func (d *Deployments) handleArtifact(ctx context.Context,
361
        multipartUploadMsg *model.MultipartUploadMsg,
362
        skipVerify bool,
363
        metadata *model.DirectUploadMetadata,
364
) (string, error) {
3✔
365

3✔
366
        l := log.FromContext(ctx)
3✔
367
        ctx, err := d.contextWithStorageSettings(ctx)
3✔
368
        if err != nil {
3✔
369
                return "", err
×
370
        }
×
371

372
        // create pipe
373
        pR, pW := io.Pipe()
3✔
374

3✔
375
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
3✔
376

3✔
377
        tee := io.TeeReader(artifactReader, pW)
3✔
378

3✔
379
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
3✔
380
        if err != nil {
5✔
381
                uid, _ = uuid.NewRandom()
2✔
382
        }
2✔
383
        artifactID := uid.String()
3✔
384

3✔
385
        ch := make(chan error)
3✔
386
        // create goroutine for artifact upload
3✔
387
        //
3✔
388
        // reading from the pipe (which is done in UploadArtifact method) is a blocking operation
3✔
389
        // and cannot be done in the same goroutine as writing to the pipe
3✔
390
        //
3✔
391
        // uploading and parsing artifact in the same process will cause in a deadlock!
3✔
392
        //nolint:errcheck
3✔
393
        go func() (err error) {
6✔
394
                defer func() { ch <- err }()
6✔
395
                if skipVerify {
5✔
396
                        err = nil
2✔
397
                        io.Copy(io.Discard, pR)
2✔
398
                        return nil
2✔
399
                }
2✔
400
                err = d.objectStorage.PutObject(
3✔
401
                        ctx, model.ImagePathFromContext(ctx, artifactID), pR,
3✔
402
                )
3✔
403
                if err != nil {
4✔
404
                        pR.CloseWithError(err)
1✔
405
                }
1✔
406
                return err
3✔
407
        }()
408

409
        // parse artifact
410
        // artifact library reads all the data from the given reader
411
        metaArtifactConstructor, err := getMetaFromArchive(&tee, skipVerify)
3✔
412
        if err != nil {
5✔
413
                _ = pW.CloseWithError(err)
2✔
414
                <-ch
2✔
415
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
2✔
416
        }
2✔
417
        validMetadata := false
2✔
418
        if skipVerify && metadata != nil {
3✔
419
                // this means we got files and metadata separately
1✔
420
                // we can now put it in the metaArtifactConstructor
1✔
421
                // after validating that the files information match the artifact
1✔
422
                validMetadata = validUpdates(metaArtifactConstructor.Updates, metadata.Updates)
1✔
423
                if validMetadata {
2✔
424
                        metaArtifactConstructor.Updates = metadata.Updates
1✔
425
                }
1✔
426
        }
427
        // validate artifact metadata
428
        if err = metaArtifactConstructor.Validate(); err != nil {
2✔
429
                return artifactID, ErrModelInvalidMetadata
×
430
        }
×
431

432
        if !skipVerify {
4✔
433
                // read the rest of the data,
2✔
434
                // just in case the artifact library did not read all the data from the reader
2✔
435
                _, err = io.Copy(io.Discard, tee)
2✔
436
                if err != nil {
2✔
437
                        // CloseWithError will cause the reading end to abort upload.
×
438
                        _ = pW.CloseWithError(err)
×
439
                        <-ch
×
440
                        return artifactID, err
×
441
                }
×
442
        }
443

444
        // close the pipe
445
        pW.Close()
2✔
446

2✔
447
        // collect output from the goroutine
2✔
448
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
2✔
449
                return artifactID, uploadResponseErr
×
450
        }
×
451

452
        size := artifactReader.Count()
2✔
453
        if skipVerify && validMetadata {
3✔
454
                size = metadata.Size
1✔
455
        }
1✔
456
        image := model.NewImage(
2✔
457
                artifactID,
2✔
458
                multipartUploadMsg.MetaConstructor,
2✔
459
                metaArtifactConstructor,
2✔
460
                size,
2✔
461
        )
2✔
462

2✔
463
        // save image structure in the system
2✔
464
        if err = d.db.InsertImage(ctx, image); err != nil {
2✔
465
                // Try to remove the storage from s3.
×
466
                if errDelete := d.objectStorage.DeleteObject(
×
467
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
468
                ); errDelete != nil {
×
469
                        l.Errorf(
×
470
                                "failed to clean up artifact storage after failure: %s",
×
471
                                errDelete,
×
472
                        )
×
473
                }
×
474
                if idxErr, ok := err.(*model.ConflictError); ok {
×
475
                        return artifactID, idxErr
×
476
                }
×
477
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
478
        }
479
        d.saveUpdateTypes(ctx, image)
2✔
480

2✔
481
        // update release
2✔
482
        if err := d.updateRelease(ctx, image, nil); err != nil {
2✔
483
                return "", err
×
484
        }
×
485

486
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
2✔
487
                return "", errors.Wrap(err, "fail to update deployments")
×
488
        }
×
489

490
        return artifactID, nil
2✔
491
}
492

493
func validUpdates(constructorUpdates []model.Update, metadataUpdates []model.Update) bool {
1✔
494
        valid := false
1✔
495
        if len(constructorUpdates) == len(metadataUpdates) {
2✔
496
                valid = true
1✔
497
                for _, update := range constructorUpdates {
2✔
498
                        for _, updateExternal := range metadataUpdates {
2✔
499
                                if !update.Match(updateExternal) {
1✔
500
                                        valid = false
×
501
                                        break
×
502
                                }
503
                        }
504
                }
505
        }
506
        return valid
1✔
507
}
508

509
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
510
// creates image structure in the system, and starts the workflow to generate the
511
// artifact from them.
512
// Returns image ID and nil on success.
513
func (d *Deployments) GenerateImage(ctx context.Context,
514
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
3✔
515

3✔
516
        if multipartGenerateImageMsg == nil {
4✔
517
                return "", ErrModelMultipartUploadMsgMalformed
1✔
518
        }
1✔
519

520
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
3✔
521
        if err != nil {
4✔
522
                return "", err
1✔
523
        }
1✔
524
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
4✔
525
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
526
        }
1✔
527
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
3✔
528
        if err != nil {
4✔
529
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
2✔
530
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
531
                }
1✔
532
                return "", err
1✔
533
        }
534

535
        return multipartGenerateImageMsg.ArtifactID, err
3✔
536
}
537

538
func (d *Deployments) GenerateConfigurationImage(
539
        ctx context.Context,
540
        deviceType string,
541
        deploymentID string,
542
) (io.Reader, error) {
2✔
543
        var buf bytes.Buffer
2✔
544
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
2✔
545
        if err != nil {
3✔
546
                return nil, err
1✔
547
        } else if dpl == nil {
4✔
548
                return nil, ErrModelDeploymentNotFound
1✔
549
        }
1✔
550
        var metaData map[string]interface{}
2✔
551
        err = json.Unmarshal(dpl.Configuration, &metaData)
2✔
552
        if err != nil {
3✔
553
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
1✔
554
        }
1✔
555

556
        artieWriter := awriter.NewWriter(&buf, artifact.NewCompressorNone())
2✔
557
        module := handlers.NewModuleImage(ArtifactConfigureType)
2✔
558
        err = artieWriter.WriteArtifact(&awriter.WriteArtifactArgs{
2✔
559
                Format:  "mender",
2✔
560
                Version: 3,
2✔
561
                Devices: []string{deviceType},
2✔
562
                Name:    dpl.ArtifactName,
2✔
563
                Updates: &awriter.Updates{Updates: []handlers.Composer{module}},
2✔
564
                Depends: &artifact.ArtifactDepends{
2✔
565
                        CompatibleDevices: []string{deviceType},
2✔
566
                },
2✔
567
                Provides: &artifact.ArtifactProvides{
2✔
568
                        ArtifactName: dpl.ArtifactName,
2✔
569
                },
2✔
570
                MetaData: metaData,
2✔
571
                TypeInfoV3: &artifact.TypeInfoV3{
2✔
572
                        Type: &ArtifactConfigureType,
2✔
573
                        ArtifactProvides: artifact.TypeInfoProvides{
2✔
574
                                ArtifactConfigureProvides: dpl.ArtifactName,
2✔
575
                        },
2✔
576
                        ArtifactDepends:        artifact.TypeInfoDepends{},
2✔
577
                        ClearsArtifactProvides: []string{ArtifactConfigureProvidesCleared},
2✔
578
                },
2✔
579
        })
2✔
580

2✔
581
        return &buf, err
2✔
582
}
583

584
// handleRawFile parses raw data, uploads it to the file storage,
585
// and starts the workflow to generate the artifact.
586
// Returns the object path to the file and nil on success.
587
func (d *Deployments) handleRawFile(ctx context.Context,
588
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
3✔
589
        l := log.FromContext(ctx)
3✔
590
        uid, _ := uuid.NewRandom()
3✔
591
        artifactID := uid.String()
3✔
592
        multipartMsg.ArtifactID = artifactID
3✔
593
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
3✔
594

3✔
595
        // check if artifact is unique
3✔
596
        // artifact is considered to be unique if there is no artifact with the same name
3✔
597
        // and supporting the same platform in the system
3✔
598
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
3✔
599
                multipartMsg.Name,
3✔
600
                multipartMsg.DeviceTypesCompatible,
3✔
601
        )
3✔
602
        if err != nil {
4✔
603
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
1✔
604
        }
1✔
605
        if !isArtifactUnique {
4✔
606
                return "", ErrModelArtifactNotUnique
1✔
607
        }
1✔
608

609
        ctx, err = d.contextWithStorageSettings(ctx)
3✔
610
        if err != nil {
3✔
611
                return "", err
×
612
        }
×
613
        err = d.objectStorage.PutObject(
3✔
614
                ctx, filePath, multipartMsg.FileReader,
3✔
615
        )
3✔
616
        if err != nil {
4✔
617
                return "", err
1✔
618
        }
1✔
619
        defer func() {
6✔
620
                if err != nil {
4✔
621
                        e := d.objectStorage.DeleteObject(ctx, filePath)
1✔
622
                        if e != nil {
2✔
623
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
1✔
624
                                        filePath, e)
1✔
625
                        }
1✔
626
                }
627
        }()
628

629
        link, err := d.objectStorage.GetRequest(
3✔
630
                ctx,
3✔
631
                filePath,
3✔
632
                path.Base(filePath),
3✔
633
                DefaultImageGenerationLinkExpire,
3✔
634
                false,
3✔
635
        )
3✔
636
        if err != nil {
4✔
637
                return "", err
1✔
638
        }
1✔
639
        multipartMsg.GetArtifactURI = link.Uri
3✔
640

3✔
641
        link, err = d.objectStorage.DeleteRequest(
3✔
642
                ctx,
3✔
643
                filePath,
3✔
644
                DefaultImageGenerationLinkExpire,
3✔
645
                false,
3✔
646
        )
3✔
647
        if err != nil {
4✔
648
                return "", err
1✔
649
        }
1✔
650
        multipartMsg.DeleteArtifactURI = link.Uri
3✔
651

3✔
652
        return artifactID, nil
3✔
653
}
654

655
// GetImage allows to fetch image object with specified id
656
// Nil if not found
657
func (d *Deployments) GetImage(ctx context.Context, id string) (*model.Image, error) {
2✔
658

2✔
659
        image, err := d.db.FindImageByID(ctx, id)
2✔
660
        if err != nil {
2✔
661
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
662
        }
×
663

664
        if image == nil {
3✔
665
                return nil, nil
1✔
666
        }
1✔
667

668
        return image, nil
2✔
669
}
670

671
// DeleteImage removes metadata and image file
672
// Noop for not existing images
673
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
674
// file is needed
675
// In case of already finished updates only image file is not needed, metadata is attached directly
676
// to device deployment therefore we still have some information about image that have been used
677
// (but not the file)
678
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
1✔
679
        found, err := d.GetImage(ctx, imageID)
1✔
680

1✔
681
        if err != nil {
1✔
682
                return errors.Wrap(err, "Getting image metadata")
×
683
        }
×
684

685
        if found == nil {
1✔
686
                return ErrImageMetaNotFound
×
687
        }
×
688

689
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
690
        if err != nil {
1✔
691
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
692
        }
×
693

694
        // Image is in use, not allowed to delete
695
        if inUse {
2✔
696
                return ErrModelImageInActiveDeployment
1✔
697
        }
1✔
698

699
        // Delete image file (call to external service)
700
        // Noop for not existing file
701
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
702
        if err != nil {
1✔
703
                return err
×
704
        }
×
705
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
706
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
707
                return errors.Wrap(err, "Deleting image file")
×
708
        }
×
709

710
        // Delete metadata
711
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
712
                return errors.Wrap(err, "Deleting image metadata")
×
713
        }
×
714

715
        // update release
716
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
717
                return err
×
718
        }
×
719

720
        return nil
1✔
721
}
722

723
// ListImages according to specified filers.
724
func (d *Deployments) ListImages(
725
        ctx context.Context,
726
        filters *model.ReleaseOrImageFilter,
727
) ([]*model.Image, int, error) {
3✔
728
        imageList, count, err := d.db.ListImages(ctx, filters)
3✔
729
        if err != nil {
4✔
730
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
1✔
731
        }
1✔
732

733
        if imageList == nil {
4✔
734
                return make([]*model.Image, 0), 0, nil
1✔
735
        }
1✔
736

737
        return imageList, count, nil
3✔
738
}
739

740
func (d *Deployments) ListImagesV2(
741
        ctx context.Context,
742
        filters *model.ImageFilter,
743
) ([]*model.Image, error) {
1✔
744
        imageList, err := d.db.ListImagesV2(ctx, filters)
1✔
745
        if err != nil {
2✔
746
                return nil, errors.Wrap(err, "Searching for image metadata")
1✔
747
        }
1✔
748

749
        if imageList == nil {
1✔
NEW
750
                return make([]*model.Image, 0), nil
×
NEW
751
        }
×
752

753
        return imageList, nil
1✔
754
}
755

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

×
760
        if err := constructor.Validate(); err != nil {
×
761
                return false, errors.Wrap(err, "Validating image metadata")
×
762
        }
×
763

764
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
765
        if err != nil {
×
766
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
767
        }
×
768

769
        if found {
×
770
                return false, ErrModelImageUsedInAnyDeployment
×
771
        }
×
772

773
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
774
        if err != nil {
×
775
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
776
        }
×
777

778
        if foundImage == nil {
×
779
                return false, nil
×
780
        }
×
781

782
        foundImage.SetModified(time.Now())
×
783
        foundImage.ImageMeta = constructor
×
784

×
785
        _, err = d.db.Update(ctx, foundImage)
×
786
        if err != nil {
×
787
                return false, errors.Wrap(err, "Updating image matadata")
×
788
        }
×
789

790
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
791
                return false, err
×
792
        }
×
793

794
        return true, nil
×
795
}
796

797
// DownloadLink presigned GET link to download image file.
798
// Returns error if image have not been uploaded.
799
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
800
        expire time.Duration) (*model.Link, error) {
1✔
801

1✔
802
        image, err := d.GetImage(ctx, imageID)
1✔
803
        if err != nil {
1✔
804
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
805
        }
×
806

807
        if image == nil {
1✔
808
                return nil, nil
×
809
        }
×
810

811
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
812
        if err != nil {
1✔
813
                return nil, err
×
814
        }
×
815
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
816
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
817
        if err != nil {
1✔
818
                return nil, errors.Wrap(err, "Searching for image file")
×
819
        }
×
820

821
        link, err := d.objectStorage.GetRequest(
1✔
822
                ctx,
1✔
823
                imagePath,
1✔
824
                image.Name+model.ArtifactFileSuffix,
1✔
825
                expire,
1✔
826
                true,
1✔
827
        )
1✔
828
        if err != nil {
1✔
829
                return nil, errors.Wrap(err, "Generating download link")
×
830
        }
×
831

832
        return link, nil
1✔
833
}
834

835
func (d *Deployments) UploadLink(
836
        ctx context.Context,
837
        expire time.Duration,
838
        skipVerify bool,
839
) (*model.UploadLink, error) {
2✔
840
        ctx, err := d.contextWithStorageSettings(ctx)
2✔
841
        if err != nil {
3✔
842
                return nil, err
1✔
843
        }
1✔
844

845
        artifactID := uuid.New().String()
2✔
846
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
2✔
847
        if skipVerify {
3✔
848
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
849
        }
1✔
850
        link, err := d.objectStorage.PutRequest(ctx, path, expire, true)
2✔
851
        if err != nil {
3✔
852
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
1✔
853
        }
1✔
854
        upLink := &model.UploadLink{
2✔
855
                ArtifactID: artifactID,
2✔
856
                IssuedAt:   time.Now(),
2✔
857
                Link:       *link,
2✔
858
        }
2✔
859
        err = d.db.InsertUploadIntent(ctx, upLink)
2✔
860
        if err != nil {
3✔
861
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
1✔
862
        }
1✔
863

864
        return upLink, err
2✔
865
}
866

867
func (d *Deployments) processUploadedArtifact(
868
        ctx context.Context,
869
        artifactID string,
870
        artifact io.ReadCloser,
871
        skipVerify bool,
872
        metadata *model.DirectUploadMetadata,
873
) error {
2✔
874
        linkStatus := model.LinkStatusCompleted
2✔
875

2✔
876
        l := log.FromContext(ctx)
2✔
877
        defer artifact.Close()
2✔
878
        ctx, cancel := context.WithCancel(ctx)
2✔
879
        defer cancel()
2✔
880
        go func() { // Heatbeat routine
4✔
881
                ticker := time.NewTicker(inprogressIdleTime / 2)
2✔
882
                done := ctx.Done()
2✔
883
                defer ticker.Stop()
2✔
884
                for {
4✔
885
                        select {
2✔
886
                        case <-ticker.C:
×
887
                                err := d.db.UpdateUploadIntentStatus(
×
888
                                        ctx,
×
889
                                        artifactID,
×
890
                                        model.LinkStatusProcessing,
×
891
                                        model.LinkStatusProcessing,
×
892
                                )
×
893
                                if err != nil {
×
894
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
895
                                        cancel()
×
896
                                        return
×
897
                                }
×
898
                        case <-done:
2✔
899
                                return
2✔
900
                        }
901
                }
902
        }()
903
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
2✔
904
                ArtifactID:     artifactID,
2✔
905
                ArtifactReader: artifact,
2✔
906
        },
2✔
907
                skipVerify,
2✔
908
                metadata,
2✔
909
        )
2✔
910
        if err != nil {
3✔
911
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
1✔
912
                linkStatus = model.LinkStatusAborted
1✔
913
        }
1✔
914
        errDB := d.db.UpdateUploadIntentStatus(
2✔
915
                ctx, artifactID,
2✔
916
                model.LinkStatusProcessing, linkStatus,
2✔
917
        )
2✔
918
        if errDB != nil {
3✔
919
                l.Warnf("failed to update upload link status: %s", errDB)
1✔
920
        }
1✔
921
        return err
2✔
922
}
923

924
func (d *Deployments) CompleteUpload(
925
        ctx context.Context,
926
        intentID string,
927
        skipVerify bool,
928
        metadata *model.DirectUploadMetadata,
929
) error {
2✔
930
        l := log.FromContext(ctx)
2✔
931
        idty := identity.FromContext(ctx)
2✔
932
        ctx, err := d.contextWithStorageSettings(ctx)
2✔
933
        if err != nil {
3✔
934
                return err
1✔
935
        }
1✔
936
        // Create an async context that doesn't cancel when server connection
937
        // closes.
938
        ctxAsync := context.Background()
2✔
939
        ctxAsync = log.WithContext(ctxAsync, l)
2✔
940
        ctxAsync = identity.WithContext(ctxAsync, idty)
2✔
941

2✔
942
        settings, _ := storage.SettingsFromContext(ctx)
2✔
943
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
2✔
944
        var artifactReader io.ReadCloser
2✔
945
        if skipVerify {
4✔
946
                artifactReader, err = d.objectStorage.GetObject(
2✔
947
                        ctxAsync,
2✔
948
                        model.ImagePathFromContext(ctx, intentID),
2✔
949
                )
2✔
950
        } else {
3✔
951
                artifactReader, err = d.objectStorage.GetObject(
1✔
952
                        ctxAsync,
1✔
953
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
1✔
954
                )
1✔
955
        }
1✔
956
        if err != nil {
3✔
957
                if errors.Is(err, storage.ErrObjectNotFound) {
2✔
958
                        return ErrUploadNotFound
1✔
959
                }
1✔
960
                return err
1✔
961
        }
962

963
        err = d.db.UpdateUploadIntentStatus(
2✔
964
                ctx,
2✔
965
                intentID,
2✔
966
                model.LinkStatusPending,
2✔
967
                model.LinkStatusProcessing,
2✔
968
        )
2✔
969
        if err != nil {
3✔
970
                errClose := artifactReader.Close()
1✔
971
                if errClose != nil {
2✔
972
                        l.Warnf("failed to close artifact reader: %s", errClose)
1✔
973
                }
1✔
974
                if errors.Is(err, store.ErrNotFound) {
2✔
975
                        return ErrUploadNotFound
1✔
976
                }
1✔
977
                return err
1✔
978
        }
979
        go d.processUploadedArtifact( // nolint:errcheck
2✔
980
                ctxAsync, intentID, artifactReader, skipVerify, metadata,
2✔
981
        )
2✔
982
        return nil
2✔
983
}
984

985
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
2✔
986
        return &model.ArtifactInfo{
2✔
987
                Format:  info.Format,
2✔
988
                Version: uint(info.Version),
2✔
989
        }
2✔
990
}
2✔
991

992
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
2✔
993
        var files []model.UpdateFile
2✔
994
        for _, u := range uFiles {
4✔
995
                files = append(files, model.UpdateFile{
2✔
996
                        Name:     u.Name,
2✔
997
                        Size:     u.Size,
2✔
998
                        Date:     &u.Date,
2✔
999
                        Checksum: string(u.Checksum),
2✔
1000
                })
2✔
1001
        }
2✔
1002
        return files, nil
2✔
1003
}
1004

1005
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
3✔
1006
        metaArtifact := model.NewArtifactMeta()
3✔
1007

3✔
1008
        aReader := areader.NewReader(*r)
3✔
1009

3✔
1010
        // There is no signature verification here.
3✔
1011
        // It is just simple check if artifact is signed or not.
3✔
1012
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
3✔
1013
                metaArtifact.Signed = true
×
1014
                return nil
×
1015
        }
×
1016

1017
        var err error
3✔
1018
        if skipVerify {
5✔
1019
                err = aReader.ReadArtifactHeaders()
2✔
1020
                if err != nil {
3✔
1021
                        return nil, errors.Wrap(err, "reading artifact error")
1✔
1022
                }
1✔
1023
        } else {
3✔
1024
                err = aReader.ReadArtifact()
3✔
1025
                if err != nil {
5✔
1026
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
1027
                }
2✔
1028
        }
1029

1030
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
2✔
1031
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
2✔
1032

2✔
1033
        metaArtifact.Name = aReader.GetArtifactName()
2✔
1034
        if metaArtifact.Info.Version == 3 {
4✔
1035
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
2✔
1036
                if err != nil {
2✔
1037
                        return nil, errors.Wrap(err,
×
1038
                                "error parsing version 3 artifact")
×
1039
                }
×
1040

1041
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
2✔
1042
                if err != nil {
2✔
1043
                        return nil, errors.Wrap(err,
×
1044
                                "error parsing version 3 artifact")
×
1045
                }
×
1046

1047
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
2✔
1048
        }
1049

1050
        for _, p := range aReader.GetHandlers() {
4✔
1051
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
2✔
1052
                if err != nil {
2✔
1053
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
1054
                }
×
1055

1056
                uMetadata, err := p.GetUpdateMetaData()
2✔
1057
                if err != nil {
2✔
1058
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
1059
                }
×
1060

1061
                metaArtifact.Updates = append(
2✔
1062
                        metaArtifact.Updates,
2✔
1063
                        model.Update{
2✔
1064
                                TypeInfo: model.ArtifactUpdateTypeInfo{
2✔
1065
                                        Type: p.GetUpdateType(),
2✔
1066
                                },
2✔
1067
                                Files:    uFiles,
2✔
1068
                                MetaData: uMetadata,
2✔
1069
                        })
2✔
1070
        }
1071

1072
        return metaArtifact, nil
2✔
1073
}
1074

1075
func getArtifactIDs(artifacts []*model.Image) []string {
3✔
1076
        artifactIDs := make([]string, 0, len(artifacts))
3✔
1077
        for _, artifact := range artifacts {
6✔
1078
                artifactIDs = append(artifactIDs, artifact.Id)
3✔
1079
        }
3✔
1080
        return artifactIDs
3✔
1081
}
1082

1083
// deployments
1084
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
2✔
1085
        ids := make([]string, len(devices))
2✔
1086
        for i, d := range devices {
4✔
1087
                ids[i] = d.ID
2✔
1088
        }
2✔
1089

1090
        return ids
2✔
1091
}
1092

1093
// updateDeploymentConstructor fills devices list with device ids
1094
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
1095
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
2✔
1096
        l := log.FromContext(ctx)
2✔
1097

2✔
1098
        id := identity.FromContext(ctx)
2✔
1099
        if id == nil {
2✔
1100
                l.Error("identity not present in the context")
×
1101
                return nil, ErrModelInternal
×
1102
        }
×
1103
        searchParams := model.SearchParams{
2✔
1104
                Page:    1,
2✔
1105
                PerPage: PerPageInventoryDevices,
2✔
1106
                Filters: []model.FilterPredicate{
2✔
1107
                        {
2✔
1108
                                Scope:     InventoryIdentityScope,
2✔
1109
                                Attribute: InventoryStatusAttributeName,
2✔
1110
                                Type:      "$eq",
2✔
1111
                                Value:     InventoryStatusAccepted,
2✔
1112
                        },
2✔
1113
                },
2✔
1114
        }
2✔
1115
        if len(constructor.Group) > 0 {
4✔
1116
                searchParams.Filters = append(
2✔
1117
                        searchParams.Filters,
2✔
1118
                        model.FilterPredicate{
2✔
1119
                                Scope:     InventoryGroupScope,
2✔
1120
                                Attribute: InventoryGroupAttributeName,
2✔
1121
                                Type:      "$eq",
2✔
1122
                                Value:     constructor.Group,
2✔
1123
                        })
2✔
1124
        }
2✔
1125

1126
        for {
4✔
1127
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
2✔
1128
                if err != nil {
3✔
1129
                        l.Errorf("error searching for devices")
1✔
1130
                        return nil, ErrModelInternal
1✔
1131
                }
1✔
1132
                if count < 1 {
3✔
1133
                        l.Errorf("no devices found")
1✔
1134
                        return nil, ErrNoDevices
1✔
1135
                }
1✔
1136
                if len(devices) < 1 {
2✔
1137
                        break
×
1138
                }
1139
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
2✔
1140
                if len(constructor.Devices) == count {
4✔
1141
                        break
2✔
1142
                }
1143
                searchParams.Page++
1✔
1144
        }
1145

1146
        return constructor, nil
2✔
1147
}
1148

1149
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1150
func (d *Deployments) CreateDeviceConfigurationDeployment(
1151
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1152
        deviceID, deploymentID string) (string, error) {
2✔
1153

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

1158
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
2✔
1159
                constructor,
2✔
1160
                deploymentID,
2✔
1161
        )
2✔
1162
        if err != nil {
2✔
1163
                return "", errors.Wrap(err, "failed to create deployment")
×
1164
        }
×
1165

1166
        deployment.DeviceList = []string{deviceID}
2✔
1167
        deployment.MaxDevices = 1
2✔
1168
        deployment.Configuration = []byte(constructor.Configuration)
2✔
1169
        deployment.Type = model.DeploymentTypeConfiguration
2✔
1170

2✔
1171
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
2✔
1172
        if err != nil {
3✔
1173
                return "", err
1✔
1174
        }
1✔
1175
        deployment.Groups = groups
2✔
1176

2✔
1177
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
4✔
1178
                if err == mongo.ErrConflictingDeployment {
3✔
1179
                        return "", ErrDuplicateDeployment
1✔
1180
                }
1✔
1181
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
3✔
1182
                        return "", ErrInvalidDeploymentID
1✔
1183
                }
1✔
1184
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1185
        }
1186

1187
        return deployment.Id, nil
2✔
1188
}
1189

1190
// CreateDeployment precomputes new deployment and schedules it for devices.
1191
func (d *Deployments) CreateDeployment(ctx context.Context,
1192
        constructor *model.DeploymentConstructor) (string, error) {
3✔
1193

3✔
1194
        var err error
3✔
1195

3✔
1196
        if constructor == nil {
4✔
1197
                return "", ErrModelMissingInput
1✔
1198
        }
1✔
1199

1200
        if err := constructor.Validate(); err != nil {
3✔
1201
                return "", errors.Wrap(err, "Validating deployment")
×
1202
        }
×
1203

1204
        if len(constructor.Group) > 0 || constructor.AllDevices {
5✔
1205
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
2✔
1206
                if err != nil {
3✔
1207
                        return "", err
1✔
1208
                }
1✔
1209
        }
1210

1211
        deployment, err := model.NewDeploymentFromConstructor(constructor)
3✔
1212
        if err != nil {
3✔
1213
                return "", errors.Wrap(err, "failed to create deployment")
×
1214
        }
×
1215

1216
        // Assign artifacts to the deployment.
1217
        // When new artifact(s) with the artifact name same as the one in the deployment
1218
        // will be uploaded to the backend, it will also become part of this deployment.
1219
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
3✔
1220
        if err != nil {
3✔
1221
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1222
        }
×
1223

1224
        if len(artifacts) == 0 {
4✔
1225
                return "", ErrNoArtifact
1✔
1226
        }
1✔
1227

1228
        deployment.Artifacts = getArtifactIDs(artifacts)
3✔
1229
        deployment.DeviceList = constructor.Devices
3✔
1230
        deployment.MaxDevices = len(constructor.Devices)
3✔
1231
        deployment.Type = model.DeploymentTypeSoftware
3✔
1232
        deployment.Filter = getDeploymentFilter(constructor)
3✔
1233
        if len(constructor.Group) > 0 {
5✔
1234
                deployment.Groups = []string{constructor.Group}
2✔
1235
        }
2✔
1236

1237
        // single device deployment case
1238
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
6✔
1239
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
3✔
1240
                if err != nil {
3✔
1241
                        return "", err
×
1242
                }
×
1243
                deployment.Groups = groups
3✔
1244
        }
1245

1246
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
5✔
1247
                if err == mongo.ErrConflictingDeployment {
4✔
1248
                        return "", ErrConflictingDeployment
2✔
1249
                }
2✔
1250
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1251
        }
1252

1253
        return deployment.Id, nil
3✔
1254
}
1255

1256
func (d *Deployments) getDeploymentGroups(
1257
        ctx context.Context,
1258
        devices []string,
1259
) ([]string, error) {
3✔
1260
        id := identity.FromContext(ctx)
3✔
1261

3✔
1262
        //only for single device deployment case
3✔
1263
        if len(devices) != 1 {
3✔
1264
                return nil, nil
×
1265
        }
×
1266

1267
        if id == nil {
3✔
1268
                id = &identity.Identity{}
×
1269
        }
×
1270

1271
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
3✔
1272
        if err != nil && err != inventory.ErrDevNotFound {
4✔
1273
                return nil, err
1✔
1274
        }
1✔
1275
        return groups, nil
3✔
1276
}
1277

1278
func getDeploymentFilter(
1279
        constructor *model.DeploymentConstructor,
1280
) *model.Filter {
3✔
1281

3✔
1282
        var filter *model.Filter
3✔
1283

3✔
1284
        if len(constructor.Group) > 0 {
5✔
1285
                filter = &model.Filter{
2✔
1286
                        Terms: []model.FilterPredicate{
2✔
1287
                                {
2✔
1288
                                        Scope:     InventoryGroupScope,
2✔
1289
                                        Attribute: InventoryGroupAttributeName,
2✔
1290
                                        Type:      "$eq",
2✔
1291
                                        Value:     constructor.Group,
2✔
1292
                                },
2✔
1293
                        },
2✔
1294
                }
2✔
1295
        } else if constructor.AllDevices {
7✔
1296
                filter = &model.Filter{
2✔
1297
                        Terms: []model.FilterPredicate{
2✔
1298
                                {
2✔
1299
                                        Scope:     InventoryIdentityScope,
2✔
1300
                                        Attribute: InventoryStatusAttributeName,
2✔
1301
                                        Type:      "$eq",
2✔
1302
                                        Value:     InventoryStatusAccepted,
2✔
1303
                                },
2✔
1304
                        },
2✔
1305
                }
2✔
1306
        } else if len(constructor.Devices) > 0 {
8✔
1307
                filter = &model.Filter{
3✔
1308
                        Terms: []model.FilterPredicate{
3✔
1309
                                {
3✔
1310
                                        Scope:     InventoryIdentityScope,
3✔
1311
                                        Attribute: InventoryIdAttributeName,
3✔
1312
                                        Type:      "$in",
3✔
1313
                                        Value:     constructor.Devices,
3✔
1314
                                },
3✔
1315
                        },
3✔
1316
                }
3✔
1317
        }
3✔
1318

1319
        return filter
3✔
1320
}
1321

1322
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1323
func (d *Deployments) IsDeploymentFinished(
1324
        ctx context.Context,
1325
        deploymentID string,
1326
) (bool, error) {
1✔
1327
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1328
        if err != nil {
1✔
1329
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1330
        }
×
1331
        if deployment == nil {
2✔
1332
                return true, nil
1✔
1333
        }
1✔
1334

1335
        return false, nil
1✔
1336
}
1337

1338
// GetDeployment fetches deployment by ID
1339
func (d *Deployments) GetDeployment(ctx context.Context,
1340
        deploymentID string) (*model.Deployment, error) {
2✔
1341

2✔
1342
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
2✔
1343
        if err != nil {
2✔
1344
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1345
        }
×
1346

1347
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
2✔
1348
                return nil, err
×
1349
        }
×
1350

1351
        return deployment, nil
2✔
1352
}
1353

1354
// ImageUsedInActiveDeployment checks if specified image is in use by deployments. Image is
1355
// considered to be in use if it's participating in at lest one non success/error deployment.
1356
func (d *Deployments) ImageUsedInActiveDeployment(ctx context.Context,
1357
        imageID string) (bool, error) {
2✔
1358

2✔
1359
        var found bool
2✔
1360

2✔
1361
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
2✔
1362
        if err != nil {
3✔
1363
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1364
        }
1✔
1365

1366
        return found, nil
2✔
1367
}
1368

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

×
1373
        var found bool
×
1374

×
1375
        found, err := d.db.ExistByArtifactId(ctx, imageID)
×
1376
        if err != nil {
×
1377
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1378
        }
×
1379

1380
        return found, nil
×
1381
}
1382

1383
// Retrieves the model.Deployment and model.DeviceDeployment structures
1384
// for the device. Upon error, nil is returned for both deployment structures.
1385
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1386
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
3✔
1387

3✔
1388
        // Retrieve device deployment
3✔
1389
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
3✔
1390

3✔
1391
        if err != nil {
3✔
1392
                return nil, nil, errors.Wrap(err,
×
1393
                        "Searching for oldest active deployment for the device")
×
1394
        } else if deviceDeployment == nil {
5✔
1395
                return d.getNewDeploymentForDevice(ctx, deviceID)
2✔
1396
        }
2✔
1397

1398
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1399
        if err != nil {
2✔
1400
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1401
        }
×
1402
        if deployment == nil {
2✔
1403
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1404
        }
×
1405

1406
        return deployment, deviceDeployment, nil
2✔
1407
}
1408

1409
// getNewDeploymentForDevice returns deployment object and creates and returns
1410
// new device deployment for the device;
1411
//
1412
// we are interested only in the deployments that are newer than the latest
1413
// deployment applied by the device;
1414
// this way we guarantee that the device will not receive deployment
1415
// that is older than the one installed on the device;
1416
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1417
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1418

2✔
1419
        var lastDeployment *time.Time
2✔
1420
        //get latest device deployment for the device;
2✔
1421
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
2✔
1422
        if err != nil {
2✔
1423
                return nil, nil, errors.Wrap(err,
×
1424
                        "Searching for latest active deployment for the device")
×
1425
        } else if deviceDeployment == nil {
4✔
1426
                lastDeployment = &time.Time{}
2✔
1427
        } else {
4✔
1428
                lastDeployment = deviceDeployment.Created
2✔
1429
        }
2✔
1430

1431
        //get deployments newer then last device deployment
1432
        //iterate over deployments and check if the device is part of the deployment or not
1433
        var deploy *model.Deployment
2✔
1434
        for lastDeployment != nil {
4✔
1435
                deploy, err = d.db.FindNewerActiveDeployment(ctx, lastDeployment, deviceID)
2✔
1436
                if err != nil {
2✔
1437
                        return nil, nil, errors.Wrap(err, "Failed to search for newer active deployments")
×
1438
                }
×
1439
                if deploy != nil {
4✔
1440
                        if deploy.MaxDevices > 0 &&
2✔
1441
                                deploy.DeviceCount != nil &&
2✔
1442
                                *deploy.DeviceCount >= deploy.MaxDevices {
2✔
1443
                                lastDeployment = deploy.Created
×
1444
                                continue
×
1445
                        }
1446
                        deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
2✔
1447
                                deviceID, deploy, model.DeviceDeploymentStatusPending)
2✔
1448
                        if err != nil {
2✔
1449
                                return nil, nil, err
×
1450
                        }
×
1451
                        return deploy, deviceDeployment, nil
2✔
1452
                } else {
2✔
1453
                        lastDeployment = nil
2✔
1454
                }
2✔
1455
        }
1456
        return nil, nil, nil
2✔
1457
}
1458

1459
func (d *Deployments) createDeviceDeploymentWithStatus(
1460
        ctx context.Context, deviceID string,
1461
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1462
) (*model.DeviceDeployment, error) {
3✔
1463
        prevStatus := model.DeviceDeploymentStatusNull
3✔
1464
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
3✔
1465
        if err != nil && err != mongo.ErrStorageNotFound {
3✔
1466
                return nil, err
×
1467
        } else if deviceDeployment != nil {
3✔
1468
                prevStatus = deviceDeployment.Status
×
1469
        }
×
1470

1471
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
3✔
1472
        deviceDeployment.Status = status
3✔
1473
        deviceDeployment.Active = status.Active()
3✔
1474
        deviceDeployment.Created = deployment.Created
3✔
1475

3✔
1476
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
3✔
1477
                return nil, err
×
1478
        }
×
1479

1480
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
3✔
1481
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
3✔
1482
                return nil, err
×
1483
        }
×
1484

1485
        if prevStatus != status {
6✔
1486
                beforeStatus := deployment.GetStatus()
3✔
1487
                // after inserting new device deployment update deployment stats
3✔
1488
                // in the database, and update deployment status
3✔
1489
                deployment.Stats, err = d.db.UpdateStatsInc(
3✔
1490
                        ctx, deployment.Id,
3✔
1491
                        prevStatus, status,
3✔
1492
                )
3✔
1493
                if err != nil {
3✔
1494
                        return nil, err
×
1495
                }
×
1496
                newStatus := deployment.GetStatus()
3✔
1497
                if beforeStatus != newStatus {
3✔
1498
                        err = d.db.SetDeploymentStatus(
×
1499
                                ctx, deployment.Id,
×
1500
                                newStatus, time.Now(),
×
1501
                        )
×
1502
                        if err != nil {
×
1503
                                return nil, errors.Wrap(err,
×
1504
                                        "failed to update deployment status")
×
1505
                        }
×
1506
                }
1507
        }
1508

1509
        if !status.Active() {
4✔
1510
                err := d.reindexDevice(ctx, deviceID)
1✔
1511
                if err != nil {
1✔
1512
                        l := log.FromContext(ctx)
×
1513
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1514
                }
×
1515
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1516
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1517
                        l := log.FromContext(ctx)
×
1518
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1519
                }
×
1520
        }
1521

1522
        return deviceDeployment, nil
3✔
1523
}
1524

1525
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1526
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1527
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
3✔
1528

3✔
1529
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
3✔
1530
        if err != nil {
3✔
1531
                return nil, ErrModelInternal
×
1532
        } else if deployment == nil {
5✔
1533
                return nil, nil
2✔
1534
        }
2✔
1535

1536
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
3✔
1537
        if err != nil {
4✔
1538
                return nil, err
1✔
1539
        }
1✔
1540
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
3✔
1541
}
1542

1543
func (d *Deployments) getDeploymentInstructions(
1544
        ctx context.Context,
1545
        deployment *model.Deployment,
1546
        deviceDeployment *model.DeviceDeployment,
1547
        request *model.DeploymentNextRequest,
1548
) (*model.DeploymentInstructions, error) {
3✔
1549

3✔
1550
        var newArtifactAssigned bool
3✔
1551

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

3✔
1554
        if deployment.Type == model.DeploymentTypeConfiguration {
4✔
1555
                // There's nothing more we need to do, the link must be filled
1✔
1556
                // in by the API layer.
1✔
1557
                return &model.DeploymentInstructions{
1✔
1558
                        ID: deployment.Id,
1✔
1559
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1560
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1561
                                // use deployment ID togheter with device ID as artifact ID
1✔
1562
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1563
                                ArtifactName:          deployment.ArtifactName,
1✔
1564
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1565
                        },
1✔
1566
                        Type: model.DeploymentTypeConfiguration,
1✔
1567
                }, nil
1✔
1568
        }
1✔
1569

1570
        // assing artifact to the device deployment
1571
        // only if it was not assgined previously
1572
        if deviceDeployment.Image == nil {
6✔
1573
                if err := d.assignArtifact(
3✔
1574
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
3✔
1575
                        return nil, err
×
1576
                }
×
1577
                newArtifactAssigned = true
3✔
1578
        }
1579

1580
        if deviceDeployment.Image == nil {
4✔
1581
                // No artifact - return empty response
1✔
1582
                return nil, nil
1✔
1583
        }
1✔
1584

1585
        // if the deployment is not forcing the installation, and
1586
        // if artifact was recognized as already installed, and this is
1587
        // a new device deployment - indicated by device deployment status "pending",
1588
        // handle already installed artifact case
1589
        if !deployment.ForceInstallation &&
3✔
1590
                d.isAlreadyInstalled(request, deviceDeployment) &&
3✔
1591
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
6✔
1592
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
3✔
1593
        }
3✔
1594

1595
        // if new artifact has been assigned to device deployment
1596
        // add artifact size to deployment total size,
1597
        // before returning deployment instruction to the device
1598
        if newArtifactAssigned {
4✔
1599
                if err := d.db.IncrementDeploymentTotalSize(
2✔
1600
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
2✔
1601
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1602
                }
×
1603
        }
1604

1605
        ctx, err := d.contextWithStorageSettings(ctx)
2✔
1606
        if err != nil {
2✔
1607
                return nil, err
×
1608
        }
×
1609

1610
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
2✔
1611
        link, err := d.objectStorage.GetRequest(
2✔
1612
                ctx,
2✔
1613
                imagePath,
2✔
1614
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
2✔
1615
                DefaultUpdateDownloadLinkExpire,
2✔
1616
                true,
2✔
1617
        )
2✔
1618
        if err != nil {
2✔
1619
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1620
        }
×
1621

1622
        instructions := &model.DeploymentInstructions{
2✔
1623
                ID: deviceDeployment.DeploymentId,
2✔
1624
                Artifact: model.ArtifactDeploymentInstructions{
2✔
1625
                        ID: deviceDeployment.Image.Id,
2✔
1626
                        ArtifactName: deviceDeployment.Image.
2✔
1627
                                ArtifactMeta.Name,
2✔
1628
                        Source: *link,
2✔
1629
                        DeviceTypesCompatible: deviceDeployment.Image.
2✔
1630
                                ArtifactMeta.DeviceTypesCompatible,
2✔
1631
                },
2✔
1632
        }
2✔
1633

2✔
1634
        return instructions, nil
2✔
1635
}
1636

1637
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1638
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
3✔
1639
        if deviceDeployment.Request != nil {
4✔
1640
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1641
                        // the device reported different device type and/or artifact name during the
1✔
1642
                        // update process, this can happen if the mender-store DB in the client is not
1✔
1643
                        // persistent so a new deployment start without a previous one is still ongoing;
1✔
1644
                        // mark deployment for this device as failed to force client to rollback
1✔
1645
                        l := log.FromContext(ctx)
1✔
1646
                        l.Errorf(
1✔
1647
                                "Device with id %s reported new data: %s during update process;"+
1✔
1648
                                        "old data: %s",
1✔
1649
                                deviceID, request, deviceDeployment.Request)
1✔
1650

1✔
1651
                        if err := d.updateDeviceDeploymentStatus(ctx, deviceDeployment,
1✔
1652
                                model.DeviceDeploymentState{
1✔
1653
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1654
                                }); err != nil {
1✔
1655
                                return errors.Wrap(err, "Failed to update deployment status")
×
1656
                        }
×
1657
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1658
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1659
                        }
×
1660
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1661
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1662
                                l := log.FromContext(ctx)
×
1663
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1664
                        }
×
1665
                        return ErrConflictingRequestData
1✔
1666
                }
1667
        } else {
3✔
1668
                // save the request
3✔
1669
                if err := d.db.SaveDeviceDeploymentRequest(
3✔
1670
                        ctx, deviceDeployment.Id, request); err != nil {
3✔
1671
                        return err
×
1672
                }
×
1673
        }
1674
        return nil
3✔
1675
}
1676

1677
// updateDeviceDeploymentStatus will update the deployment status for device of
1678
// ID `deviceID`. Returns nil if update was successful.
1679
func (d *Deployments) UpdateDeviceDeploymentStatus(
1680
        ctx context.Context,
1681
        deviceID, deploymentID string,
1682
        ddState model.DeviceDeploymentState,
1683
) error {
3✔
1684
        deviceDeployment, err := d.db.GetDeviceDeployment(
3✔
1685
                ctx, deviceID, deploymentID, false,
3✔
1686
        )
3✔
1687
        if err == mongo.ErrStorageNotFound {
5✔
1688
                return ErrStorageNotFound
2✔
1689
        } else if err != nil {
5✔
1690
                return err
×
1691
        }
×
1692

1693
        return d.updateDeviceDeploymentStatus(ctx, deviceDeployment, ddState)
3✔
1694
}
1695

1696
func (d *Deployments) updateDeviceDeploymentStatus(
1697
        ctx context.Context,
1698
        dd *model.DeviceDeployment,
1699
        ddState model.DeviceDeploymentState,
1700
) error {
3✔
1701

3✔
1702
        l := log.FromContext(ctx)
3✔
1703

3✔
1704
        l.Infof("New status: %s for device %s deployment: %v",
3✔
1705
                ddState.Status, dd.DeviceId, dd.DeploymentId,
3✔
1706
        )
3✔
1707

3✔
1708
        var finishTime *time.Time = nil
3✔
1709
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
6✔
1710
                now := time.Now()
3✔
1711
                finishTime = &now
3✔
1712
        }
3✔
1713

1714
        currentStatus := dd.Status
3✔
1715

3✔
1716
        if currentStatus == model.DeviceDeploymentStatusAborted {
3✔
1717
                return ErrDeploymentAborted
×
1718
        }
×
1719

1720
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
3✔
1721
                return ErrDeviceDecommissioned
×
1722
        }
×
1723

1724
        // nothing to do
1725
        if ddState.Status == currentStatus {
3✔
1726
                return nil
×
1727
        }
×
1728

1729
        // update finish time
1730
        ddState.FinishTime = finishTime
3✔
1731

3✔
1732
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
3✔
1733
                dd.DeviceId, dd.DeploymentId, ddState, dd.Status)
3✔
1734
        if err != nil {
3✔
1735
                return err
×
1736
        }
×
1737

1738
        if old != ddState.Status {
6✔
1739
                // fetch deployment stats and update deployment status
3✔
1740
                deployment, err := d.db.FindDeploymentByID(ctx, dd.DeploymentId)
3✔
1741
                if err != nil {
3✔
1742
                        return errors.Wrap(err, "failed when searching for deployment")
×
1743
                }
×
1744
                if deployment == nil {
4✔
1745
                        return ErrModelDeploymentNotFound
1✔
1746
                }
1✔
1747
                beforeStatus := deployment.GetStatus()
3✔
1748

3✔
1749
                deployment.Stats, err = d.db.UpdateStatsInc(ctx, dd.DeploymentId, old, ddState.Status)
3✔
1750
                if err != nil {
3✔
1751
                        return err
×
1752
                }
×
1753
                newStatus := deployment.GetStatus()
3✔
1754
                if beforeStatus != newStatus {
6✔
1755
                        err = d.db.SetDeploymentStatus(ctx, dd.DeploymentId, newStatus, time.Now())
3✔
1756
                        if err != nil {
3✔
1757
                                return errors.Wrap(err, "failed to update deployment status")
×
1758
                        }
×
1759
                }
1760
        }
1761

1762
        if !ddState.Status.Active() {
6✔
1763
                l := log.FromContext(ctx)
3✔
1764
                ldd := model.DeviceDeployment{
3✔
1765
                        DeviceId:     dd.DeviceId,
3✔
1766
                        DeploymentId: dd.DeploymentId,
3✔
1767
                        Id:           dd.Id,
3✔
1768
                        Status:       ddState.Status,
3✔
1769
                }
3✔
1770
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
3✔
1771
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1772
                }
×
1773
                if err := d.reindexDevice(ctx, dd.DeviceId); err != nil {
3✔
1774
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1775
                }
×
1776
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
3✔
1777
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1778
                }
×
1779
        }
1780

1781
        return nil
3✔
1782
}
1783

1784
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1785
        deploymentID string) (model.Stats, error) {
1✔
1786

1✔
1787
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1788

1✔
1789
        if err != nil {
1✔
1790
                return nil, errors.Wrap(err, "checking deployment id")
×
1791
        }
×
1792

1793
        if deployment == nil {
1✔
1794
                return nil, nil
×
1795
        }
×
1796

1797
        return deployment.Stats, nil
1✔
1798
}
1799
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1800
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1801

×
1802
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1803

×
1804
        if err != nil {
×
1805
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1806
        }
×
1807

1808
        if deploymentStats == nil {
×
1809
                return nil, ErrModelDeploymentNotFound
×
1810
        }
×
1811

1812
        return deploymentStats, nil
×
1813
}
1814

1815
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1816
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1817
        deploymentID string) ([]model.DeviceDeployment, error) {
2✔
1818

2✔
1819
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
2✔
1820
        if err != nil {
2✔
1821
                return nil, ErrModelInternal
×
1822
        }
×
1823

1824
        if deployment == nil {
2✔
1825
                return nil, ErrModelDeploymentNotFound
×
1826
        }
×
1827

1828
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
2✔
1829
        if err != nil {
2✔
1830
                return nil, ErrModelInternal
×
1831
        }
×
1832

1833
        return statuses, nil
2✔
1834
}
1835

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

1✔
1839
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1840
        if err != nil {
1✔
1841
                return nil, -1, ErrModelInternal
×
1842
        }
×
1843

1844
        if deployment == nil {
1✔
1845
                return nil, -1, ErrModelDeploymentNotFound
×
1846
        }
×
1847

1848
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1849
        if err != nil {
1✔
1850
                return nil, -1, ErrModelInternal
×
1851
        }
×
1852

1853
        return statuses, totalCount, nil
1✔
1854
}
1855

1856
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1857
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
2✔
1858
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
2✔
1859
        if err != nil {
3✔
1860
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1861
        }
1✔
1862

1863
        deploymentIDs := make([]string, len(deviceDeployments))
2✔
1864
        for i, deviceDeployment := range deviceDeployments {
4✔
1865
                deploymentIDs[i] = deviceDeployment.DeploymentId
2✔
1866
        }
2✔
1867
        var deployments []*model.Deployment
2✔
1868
        if len(deviceDeployments) > 0 {
4✔
1869
                deployments, _, err = d.db.FindDeployments(ctx, model.Query{
2✔
1870
                        IDs:          deploymentIDs,
2✔
1871
                        Limit:        len(deviceDeployments),
2✔
1872
                        DisableCount: true,
2✔
1873
                })
2✔
1874
                if err != nil {
3✔
1875
                        return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1876
                }
1✔
1877
        }
1878

1879
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1880
        for _, deployment := range deployments {
4✔
1881
                deploymentsMap[deployment.Id] = deployment
2✔
1882
        }
2✔
1883

1884
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1885
        for i, deviceDeployment := range deviceDeployments {
4✔
1886
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
4✔
1887
                        res = append(res, model.DeviceDeploymentListItem{
2✔
1888
                                Id:         deviceDeployment.Id,
2✔
1889
                                Deployment: deployment,
2✔
1890
                                Device:     &deviceDeployments[i],
2✔
1891
                        })
2✔
1892
                } else {
3✔
1893
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1894
                                Id:     deviceDeployment.Id,
1✔
1895
                                Device: &deviceDeployments[i],
1✔
1896
                        })
1✔
1897
                }
1✔
1898
        }
1899

1900
        return res, totalCount, nil
2✔
1901
}
1902

1903
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1904
        ctx context.Context,
1905
        deployment *model.Deployment,
1906
) error {
3✔
1907
        if deployment.DeviceCount == nil {
3✔
1908
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1909
                if err != nil {
×
1910
                        return errors.Wrap(err, "counting device deployments")
×
1911
                }
×
1912
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1913
                if err != nil {
×
1914
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1915
                }
×
1916
                deployment.DeviceCount = &deviceCount
×
1917
        }
1918

1919
        return nil
3✔
1920
}
1921

1922
func (d *Deployments) LookupDeployment(ctx context.Context,
1923
        query model.Query) ([]*model.Deployment, int64, error) {
3✔
1924
        list, totalCount, err := d.db.FindDeployments(ctx, query)
3✔
1925

3✔
1926
        if err != nil {
4✔
1927
                return nil, 0, errors.Wrap(err, "searching for deployments")
1✔
1928
        }
1✔
1929

1930
        if list == nil {
6✔
1931
                return make([]*model.Deployment, 0), 0, nil
3✔
1932
        }
3✔
1933

1934
        for _, deployment := range list {
4✔
1935
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
2✔
1936
                        return nil, 0, err
×
1937
                }
×
1938
        }
1939

1940
        return list, totalCount, nil
2✔
1941
}
1942

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

1✔
1948
        // repack to temporary deployment log and validate
1✔
1949
        dlog := model.DeploymentLog{
1✔
1950
                DeviceID:     deviceID,
1✔
1951
                DeploymentID: deploymentID,
1✔
1952
                Messages:     logs,
1✔
1953
        }
1✔
1954
        if err := dlog.Validate(); err != nil {
1✔
1955
                return errors.Wrap(err, ErrStorageInvalidLog.Error())
×
1956
        }
×
1957

1958
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1959
                if err != nil {
×
1960
                        return err
×
1961
                } else {
×
1962
                        return ErrModelDeploymentNotFound
×
1963
                }
×
1964
        }
1965

1966
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1967
                return err
×
1968
        }
×
1969

1970
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1971
                deviceID, deploymentID, true)
1✔
1972
}
1973

1974
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1975
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1976

1✔
1977
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1978
                deviceID, deploymentID)
1✔
1979
}
1✔
1980

1981
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1982
        deploymentID string, deviceID string) (bool, error) {
1✔
1983
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1984
}
1✔
1985

1986
// AbortDeployment aborts deployment for devices and updates deployment stats
1987
func (d *Deployments) AbortDeployment(ctx context.Context, deploymentID string) error {
2✔
1988

2✔
1989
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
3✔
1990
                return err
1✔
1991
        }
1✔
1992

1993
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
2✔
1994
                ctx, deploymentID)
2✔
1995
        if err != nil {
3✔
1996
                return err
1✔
1997
        }
1✔
1998

1999
        // update statistics
2000
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
3✔
2001
                return errors.Wrap(err, "failed to update deployment stats")
1✔
2002
        }
1✔
2003

2004
        // when aborting the deployment we need to set status directly instead of
2005
        // using recalcDeploymentStatus method;
2006
        // it is possible that the deployment does not have any device deployments yet;
2007
        // in that case, all statistics are 0 and calculating status based on statistics
2008
        // will not work - the calculated status will be "pending"
2009
        if err := d.db.SetDeploymentStatus(ctx,
2✔
2010
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
2✔
2011
                return errors.Wrap(err, "failed to update deployment status")
×
2012
        }
×
2013

2014
        return nil
2✔
2015
}
2016

2017
func (d *Deployments) updateDeviceDeploymentsStatus(
2018
        ctx context.Context,
2019
        deviceId string,
2020
        status model.DeviceDeploymentStatus,
2021
) error {
2✔
2022
        var latestDeployment *time.Time
2✔
2023
        // Retrieve active device deployment for the device
2✔
2024
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
2✔
2025
        if err != nil {
3✔
2026
                return errors.Wrap(err, "Searching for active deployment for the device")
1✔
2027
        } else if deviceDeployment != nil {
4✔
2028
                now := time.Now()
1✔
2029
                ddStatus := model.DeviceDeploymentState{
1✔
2030
                        Status:     status,
1✔
2031
                        FinishTime: &now,
1✔
2032
                }
1✔
2033
                if err := d.updateDeviceDeploymentStatus(
1✔
2034
                        ctx, deviceDeployment, ddStatus,
1✔
2035
                ); err != nil {
1✔
2036
                        return errors.Wrap(err, "updating device deployment status")
×
2037
                }
×
2038
                latestDeployment = deviceDeployment.Created
1✔
2039
        } else {
2✔
2040
                // get latest device deployment for the device
2✔
2041
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
2✔
2042
                if err != nil {
2✔
2043
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
2044
                } else if deviceDeployment == nil {
4✔
2045
                        latestDeployment = &time.Time{}
2✔
2046
                } else {
3✔
2047
                        latestDeployment = deviceDeployment.Created
1✔
2048
                }
1✔
2049
        }
2050

2051
        // get deployments newer then last device deployment
2052
        // iterate over deployments and check if the device is part of the deployment or not
2053
        // if the device is part of the deployment create new, decommisioned device deployment
2054
        var deploy *model.Deployment
2✔
2055
        deploy, err = d.db.FindNewerActiveDeployment(ctx, latestDeployment, deviceId)
2✔
2056
        if err != nil {
2✔
2057
                return errors.Wrap(err, "Failed to search for newer active deployments")
×
2058
        }
×
2059
        if deploy != nil {
3✔
2060
                deviceDeployment, err = d.createDeviceDeploymentWithStatus(ctx,
1✔
2061
                        deviceId, deploy, status)
1✔
2062
                if err != nil {
1✔
2063
                        return err
×
2064
                }
×
2065
                if !status.Active() {
2✔
2066
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
2067
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
2068
                                l := log.FromContext(ctx)
×
2069
                                l.Warn(errors.Wrap(err, "failed to trigger a deployment reindex"))
×
2070
                        }
×
2071
                }
2072
        }
2073

2074
        if err := d.reindexDevice(ctx, deviceId); err != nil {
2✔
2075
                l := log.FromContext(ctx)
×
2076
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
2077
        }
×
2078

2079
        return nil
2✔
2080
}
2081

2082
// DecommissionDevice updates the status of all the pending and active deployments for a device
2083
// to decommissioned
2084
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
2✔
2085
        return d.updateDeviceDeploymentsStatus(
2✔
2086
                ctx,
2✔
2087
                deviceId,
2✔
2088
                model.DeviceDeploymentStatusDecommissioned,
2✔
2089
        )
2✔
2090
}
2✔
2091

2092
// AbortDeviceDeployments aborts all the pending and active deployments for a device
2093
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
1✔
2094
        return d.updateDeviceDeploymentsStatus(
1✔
2095
                ctx,
1✔
2096
                deviceId,
1✔
2097
                model.DeviceDeploymentStatusAborted,
1✔
2098
        )
1✔
2099
}
1✔
2100

2101
// DeleteDeviceDeploymentsHistory deletes the device deployments history
2102
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
1✔
2103
        // get device deployments which will be marked as deleted
1✔
2104
        f := false
1✔
2105
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
1✔
2106
        if err != nil {
1✔
2107
                return err
×
2108
        }
×
2109

2110
        // no device deployments to update
2111
        if len(dd) <= 0 {
1✔
2112
                return nil
×
2113
        }
×
2114

2115
        // mark device deployments as deleted
2116
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
2✔
2117
                return err
1✔
2118
        }
1✔
2119

2120
        // trigger reindexing of updated device deployments
2121
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2122
        for i, d := range dd {
2✔
2123
                deviceDeployments[i].ID = d.Id
1✔
2124
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2125
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2126
        }
1✔
2127
        if d.reportingClient != nil {
2✔
2128
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2129
        }
1✔
2130
        return err
1✔
2131
}
2132

2133
// Storage settings
2134
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
2135
        settings, err := d.db.GetStorageSettings(ctx)
3✔
2136
        if err != nil {
4✔
2137
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2138
        }
1✔
2139

2140
        return settings, nil
3✔
2141
}
2142

2143
func (d *Deployments) SetStorageSettings(
2144
        ctx context.Context,
2145
        storageSettings *model.StorageSettings,
2146
) error {
2✔
2147
        if storageSettings != nil {
4✔
2148
                ctx = storage.SettingsWithContext(ctx, storageSettings)
2✔
2149
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
2✔
2150
                        return errors.WithMessage(err,
×
2151
                                "the provided storage settings failed the health check",
×
2152
                        )
×
2153
                }
×
2154
        }
2155
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
3✔
2156
                return errors.Wrap(err, "Failed to save settings")
1✔
2157
        }
1✔
2158

2159
        return nil
2✔
2160
}
2161

2162
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
1✔
2163
        d.reportingClient = c
1✔
2164
        return d
1✔
2165
}
1✔
2166

2167
func (d *Deployments) haveReporting() bool {
2✔
2168
        return d.reportingClient != nil
2✔
2169
}
2✔
2170

2171
func (d *Deployments) search(
2172
        ctx context.Context,
2173
        tid string,
2174
        parms model.SearchParams,
2175
) ([]model.InvDevice, int, error) {
2✔
2176
        if d.haveReporting() {
3✔
2177
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2178
        } else {
3✔
2179
                return d.inventoryClient.Search(ctx, tid, parms)
2✔
2180
        }
2✔
2181
}
2182

2183
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2184
        ctx context.Context,
2185
        artifactName string,
2186
) error {
3✔
2187
        // first check if there are pending deployments with given artifact name
3✔
2188
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
3✔
2189
        if err != nil {
3✔
2190
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
2191
        }
×
2192
        if !exists {
5✔
2193
                return nil
2✔
2194
        }
2✔
2195

2196
        // Assign artifacts to the deployments with given artifact name
2197
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2198
        if err != nil {
1✔
2199
                return errors.Wrap(err, "Finding artifact with given name")
×
2200
        }
×
2201

2202
        if len(artifacts) == 0 {
1✔
2203
                return ErrNoArtifact
×
2204
        }
×
2205
        artifactIDs := getArtifactIDs(artifacts)
1✔
2206
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2207
}
2208

2209
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
3✔
2210
        if d.reportingClient != nil {
4✔
2211
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
1✔
2212
        }
1✔
2213
        return nil
3✔
2214
}
2215

2216
func (d *Deployments) reindexDeployment(ctx context.Context,
2217
        deviceID, deploymentID, ID string) error {
3✔
2218
        if d.reportingClient != nil {
4✔
2219
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
1✔
2220
        }
1✔
2221
        return nil
3✔
2222
}
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