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

mendersoftware / mender-server / 1781610070

23 Apr 2025 09:54AM UTC coverage: 65.305% (+0.04%) from 65.262%
1781610070

Pull #597

gitlab-ci

alfrunes
ci: Retract 8c8028081 from changelog

The changelog is superseded by ec713ae42

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #597: MEN-7744: Rate limit authenticated devices API

27 of 54 new or added lines in 3 files covered. (50.0%)

64 existing lines in 3 files now uncovered.

31824 of 48731 relevant lines covered (65.31%)

1.37 hits per line

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

78.73
/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
        DownloadLink(ctx context.Context, imageID string,
130
                expire time.Duration) (*model.Link, error)
131
        UploadLink(
132
                ctx context.Context,
133
                expire time.Duration,
134
                skipVerify bool,
135
        ) (*model.UploadLink, error)
136
        CompleteUpload(
137
                ctx context.Context,
138
                intentID string,
139
                skipVerify bool,
140
                metadata *model.DirectUploadMetadata,
141
        ) error
142
        GetImage(ctx context.Context, id string) (*model.Image, error)
143
        DeleteImage(ctx context.Context, imageID string) error
144
        CreateImage(ctx context.Context,
145
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
146
        GenerateImage(ctx context.Context,
147
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
148
        GenerateConfigurationImage(
149
                ctx context.Context,
150
                deviceType string,
151
                deploymentID string,
152
        ) (io.Reader, error)
153
        EditImage(ctx context.Context, id string,
154
                constructorData *model.ImageMeta) (bool, error)
155

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

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

210
type Deployments struct {
211
        db              store.DataStore
212
        objectStorage   storage.ObjectStorage
213
        workflowsClient workflows.Client
214
        inventoryClient inventory.Client
215
        reportingClient reporting.Client
216
}
217

218
// Compile-time check
219
var _ App = &Deployments{}
220

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

235
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
1✔
236
        d.workflowsClient = workflowsClient
1✔
237
}
1✔
238

239
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
1✔
240
        d.inventoryClient = inventoryClient
1✔
241
}
1✔
242

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

256
        err = d.workflowsClient.CheckHealth(ctx)
2✔
257
        if err != nil {
3✔
258
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
259
        }
1✔
260

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

266
        if d.reportingClient != nil {
3✔
267
                err = d.reportingClient.CheckHealth(ctx)
1✔
268
                if err != nil {
2✔
269
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
270
                }
1✔
271
        }
272
        return nil
2✔
273
}
274

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

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

1✔
309
        } else if err != nil {
3✔
310
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
311
        }
1✔
312
        return limit, nil
1✔
313
}
314

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

320
        return nil
2✔
321
}
322

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

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

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

3✔
362
        l := log.FromContext(ctx)
3✔
363
        ctx, err := d.contextWithStorageSettings(ctx)
3✔
364
        if err != nil {
3✔
365
                return "", err
×
366
        }
×
367

368
        // create pipe
369
        pR, pW := io.Pipe()
3✔
370

3✔
371
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
3✔
372

3✔
373
        tee := io.TeeReader(artifactReader, pW)
3✔
374

3✔
375
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
3✔
376
        if err != nil {
5✔
377
                uid, _ = uuid.NewRandom()
2✔
378
        }
2✔
379
        artifactID := uid.String()
3✔
380

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

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

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

440
        // close the pipe
441
        pW.Close()
2✔
442

2✔
443
        // collect output from the goroutine
2✔
444
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
2✔
445
                return artifactID, uploadResponseErr
×
446
        }
×
447

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

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

2✔
477
        // update release
2✔
478
        if err := d.updateRelease(ctx, image, nil); err != nil {
2✔
479
                return "", err
×
480
        }
×
481

482
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
2✔
483
                return "", errors.Wrap(err, "fail to update deployments")
×
484
        }
×
485

486
        return artifactID, nil
2✔
487
}
488

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

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

3✔
512
        if multipartGenerateImageMsg == nil {
4✔
513
                return "", ErrModelMultipartUploadMsgMalformed
1✔
514
        }
1✔
515

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

531
        return multipartGenerateImageMsg.ArtifactID, err
3✔
532
}
533

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

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

2✔
577
        return &buf, err
2✔
578
}
579

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

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

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

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

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

3✔
648
        return artifactID, nil
3✔
649
}
650

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

2✔
655
        image, err := d.db.FindImageByID(ctx, id)
2✔
656
        if err != nil {
2✔
657
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
658
        }
×
659

660
        if image == nil {
3✔
661
                return nil, nil
1✔
662
        }
1✔
663

664
        return image, nil
2✔
665
}
666

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

1✔
677
        if err != nil {
1✔
678
                return errors.Wrap(err, "Getting image metadata")
×
679
        }
×
680

681
        if found == nil {
1✔
682
                return ErrImageMetaNotFound
×
683
        }
×
684

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

690
        // Image is in use, not allowed to delete
691
        if inUse {
2✔
692
                return ErrModelImageInActiveDeployment
1✔
693
        }
1✔
694

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

706
        // Delete metadata
707
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
708
                return errors.Wrap(err, "Deleting image metadata")
×
709
        }
×
710

711
        // update release
712
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
713
                return err
×
714
        }
×
715

716
        return nil
1✔
717
}
718

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

729
        if imageList == nil {
3✔
730
                return make([]*model.Image, 0), 0, nil
1✔
731
        }
1✔
732

733
        return imageList, count, nil
2✔
734
}
735

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

×
740
        if err := constructor.Validate(); err != nil {
×
741
                return false, errors.Wrap(err, "Validating image metadata")
×
742
        }
×
743

744
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
745
        if err != nil {
×
746
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
747
        }
×
748

749
        if found {
×
750
                return false, ErrModelImageUsedInAnyDeployment
×
751
        }
×
752

753
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
754
        if err != nil {
×
755
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
756
        }
×
757

758
        if foundImage == nil {
×
759
                return false, nil
×
760
        }
×
761

762
        foundImage.SetModified(time.Now())
×
763
        foundImage.ImageMeta = constructor
×
764

×
765
        _, err = d.db.Update(ctx, foundImage)
×
766
        if err != nil {
×
767
                return false, errors.Wrap(err, "Updating image matadata")
×
768
        }
×
769

770
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
771
                return false, err
×
772
        }
×
773

774
        return true, nil
×
775
}
776

777
// DownloadLink presigned GET link to download image file.
778
// Returns error if image have not been uploaded.
779
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
780
        expire time.Duration) (*model.Link, error) {
1✔
781

1✔
782
        image, err := d.GetImage(ctx, imageID)
1✔
783
        if err != nil {
1✔
784
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
785
        }
×
786

787
        if image == nil {
1✔
788
                return nil, nil
×
789
        }
×
790

791
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
792
        if err != nil {
1✔
793
                return nil, err
×
794
        }
×
795
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
796
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
797
        if err != nil {
1✔
798
                return nil, errors.Wrap(err, "Searching for image file")
×
799
        }
×
800

801
        link, err := d.objectStorage.GetRequest(
1✔
802
                ctx,
1✔
803
                imagePath,
1✔
804
                image.Name+model.ArtifactFileSuffix,
1✔
805
                expire,
1✔
806
                true,
1✔
807
        )
1✔
808
        if err != nil {
1✔
809
                return nil, errors.Wrap(err, "Generating download link")
×
810
        }
×
811

812
        return link, nil
1✔
813
}
814

815
func (d *Deployments) UploadLink(
816
        ctx context.Context,
817
        expire time.Duration,
818
        skipVerify bool,
819
) (*model.UploadLink, error) {
2✔
820
        ctx, err := d.contextWithStorageSettings(ctx)
2✔
821
        if err != nil {
3✔
822
                return nil, err
1✔
823
        }
1✔
824

825
        artifactID := uuid.New().String()
2✔
826
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
2✔
827
        if skipVerify {
3✔
828
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
829
        }
1✔
830
        link, err := d.objectStorage.PutRequest(ctx, path, expire, true)
2✔
831
        if err != nil {
3✔
832
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
1✔
833
        }
1✔
834
        upLink := &model.UploadLink{
2✔
835
                ArtifactID: artifactID,
2✔
836
                IssuedAt:   time.Now(),
2✔
837
                Link:       *link,
2✔
838
        }
2✔
839
        err = d.db.InsertUploadIntent(ctx, upLink)
2✔
840
        if err != nil {
3✔
841
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
1✔
842
        }
1✔
843

844
        return upLink, err
2✔
845
}
846

847
func (d *Deployments) processUploadedArtifact(
848
        ctx context.Context,
849
        artifactID string,
850
        artifact io.ReadCloser,
851
        skipVerify bool,
852
        metadata *model.DirectUploadMetadata,
853
) error {
2✔
854
        linkStatus := model.LinkStatusCompleted
2✔
855

2✔
856
        l := log.FromContext(ctx)
2✔
857
        defer artifact.Close()
2✔
858
        ctx, cancel := context.WithCancel(ctx)
2✔
859
        defer cancel()
2✔
860
        go func() { // Heatbeat routine
4✔
861
                ticker := time.NewTicker(inprogressIdleTime / 2)
2✔
862
                done := ctx.Done()
2✔
863
                defer ticker.Stop()
2✔
864
                for {
4✔
865
                        select {
2✔
866
                        case <-ticker.C:
×
867
                                err := d.db.UpdateUploadIntentStatus(
×
868
                                        ctx,
×
869
                                        artifactID,
×
870
                                        model.LinkStatusProcessing,
×
871
                                        model.LinkStatusProcessing,
×
872
                                )
×
873
                                if err != nil {
×
874
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
875
                                        cancel()
×
876
                                        return
×
877
                                }
×
878
                        case <-done:
2✔
879
                                return
2✔
880
                        }
881
                }
882
        }()
883
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
2✔
884
                ArtifactID:     artifactID,
2✔
885
                ArtifactReader: artifact,
2✔
886
        },
2✔
887
                skipVerify,
2✔
888
                metadata,
2✔
889
        )
2✔
890
        if err != nil {
3✔
891
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
1✔
892
                linkStatus = model.LinkStatusAborted
1✔
893
        }
1✔
894
        errDB := d.db.UpdateUploadIntentStatus(
2✔
895
                ctx, artifactID,
2✔
896
                model.LinkStatusProcessing, linkStatus,
2✔
897
        )
2✔
898
        if errDB != nil {
3✔
899
                l.Warnf("failed to update upload link status: %s", errDB)
1✔
900
        }
1✔
901
        return err
2✔
902
}
903

904
func (d *Deployments) CompleteUpload(
905
        ctx context.Context,
906
        intentID string,
907
        skipVerify bool,
908
        metadata *model.DirectUploadMetadata,
909
) error {
2✔
910
        l := log.FromContext(ctx)
2✔
911
        idty := identity.FromContext(ctx)
2✔
912
        ctx, err := d.contextWithStorageSettings(ctx)
2✔
913
        if err != nil {
3✔
914
                return err
1✔
915
        }
1✔
916
        // Create an async context that doesn't cancel when server connection
917
        // closes.
918
        ctxAsync := context.Background()
2✔
919
        ctxAsync = log.WithContext(ctxAsync, l)
2✔
920
        ctxAsync = identity.WithContext(ctxAsync, idty)
2✔
921

2✔
922
        settings, _ := storage.SettingsFromContext(ctx)
2✔
923
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
2✔
924
        var artifactReader io.ReadCloser
2✔
925
        if skipVerify {
4✔
926
                artifactReader, err = d.objectStorage.GetObject(
2✔
927
                        ctxAsync,
2✔
928
                        model.ImagePathFromContext(ctx, intentID),
2✔
929
                )
2✔
930
        } else {
3✔
931
                artifactReader, err = d.objectStorage.GetObject(
1✔
932
                        ctxAsync,
1✔
933
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
1✔
934
                )
1✔
935
        }
1✔
936
        if err != nil {
3✔
937
                if errors.Is(err, storage.ErrObjectNotFound) {
2✔
938
                        return ErrUploadNotFound
1✔
939
                }
1✔
940
                return err
1✔
941
        }
942

943
        err = d.db.UpdateUploadIntentStatus(
2✔
944
                ctx,
2✔
945
                intentID,
2✔
946
                model.LinkStatusPending,
2✔
947
                model.LinkStatusProcessing,
2✔
948
        )
2✔
949
        if err != nil {
3✔
950
                errClose := artifactReader.Close()
1✔
951
                if errClose != nil {
2✔
952
                        l.Warnf("failed to close artifact reader: %s", errClose)
1✔
953
                }
1✔
954
                if errors.Is(err, store.ErrNotFound) {
2✔
955
                        return ErrUploadNotFound
1✔
956
                }
1✔
957
                return err
1✔
958
        }
959
        go d.processUploadedArtifact( // nolint:errcheck
2✔
960
                ctxAsync, intentID, artifactReader, skipVerify, metadata,
2✔
961
        )
2✔
962
        return nil
2✔
963
}
964

965
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
2✔
966
        return &model.ArtifactInfo{
2✔
967
                Format:  info.Format,
2✔
968
                Version: uint(info.Version),
2✔
969
        }
2✔
970
}
2✔
971

972
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
2✔
973
        var files []model.UpdateFile
2✔
974
        for _, u := range uFiles {
4✔
975
                files = append(files, model.UpdateFile{
2✔
976
                        Name:     u.Name,
2✔
977
                        Size:     u.Size,
2✔
978
                        Date:     &u.Date,
2✔
979
                        Checksum: string(u.Checksum),
2✔
980
                })
2✔
981
        }
2✔
982
        return files, nil
2✔
983
}
984

985
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
3✔
986
        metaArtifact := model.NewArtifactMeta()
3✔
987

3✔
988
        aReader := areader.NewReader(*r)
3✔
989

3✔
990
        // There is no signature verification here.
3✔
991
        // It is just simple check if artifact is signed or not.
3✔
992
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
3✔
993
                metaArtifact.Signed = true
×
994
                return nil
×
995
        }
×
996

997
        var err error
3✔
998
        if skipVerify {
5✔
999
                err = aReader.ReadArtifactHeaders()
2✔
1000
                if err != nil {
3✔
1001
                        return nil, errors.Wrap(err, "reading artifact error")
1✔
1002
                }
1✔
1003
        } else {
3✔
1004
                err = aReader.ReadArtifact()
3✔
1005
                if err != nil {
5✔
1006
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
1007
                }
2✔
1008
        }
1009

1010
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
2✔
1011
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
2✔
1012

2✔
1013
        metaArtifact.Name = aReader.GetArtifactName()
2✔
1014
        if metaArtifact.Info.Version == 3 {
4✔
1015
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
2✔
1016
                if err != nil {
2✔
1017
                        return nil, errors.Wrap(err,
×
1018
                                "error parsing version 3 artifact")
×
1019
                }
×
1020

1021
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
2✔
1022
                if err != nil {
2✔
1023
                        return nil, errors.Wrap(err,
×
1024
                                "error parsing version 3 artifact")
×
1025
                }
×
1026

1027
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
2✔
1028
        }
1029

1030
        for _, p := range aReader.GetHandlers() {
4✔
1031
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
2✔
1032
                if err != nil {
2✔
1033
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
1034
                }
×
1035

1036
                uMetadata, err := p.GetUpdateMetaData()
2✔
1037
                if err != nil {
2✔
1038
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
1039
                }
×
1040

1041
                metaArtifact.Updates = append(
2✔
1042
                        metaArtifact.Updates,
2✔
1043
                        model.Update{
2✔
1044
                                TypeInfo: model.ArtifactUpdateTypeInfo{
2✔
1045
                                        Type: p.GetUpdateType(),
2✔
1046
                                },
2✔
1047
                                Files:    uFiles,
2✔
1048
                                MetaData: uMetadata,
2✔
1049
                        })
2✔
1050
        }
1051

1052
        return metaArtifact, nil
2✔
1053
}
1054

1055
func getArtifactIDs(artifacts []*model.Image) []string {
3✔
1056
        artifactIDs := make([]string, 0, len(artifacts))
3✔
1057
        for _, artifact := range artifacts {
6✔
1058
                artifactIDs = append(artifactIDs, artifact.Id)
3✔
1059
        }
3✔
1060
        return artifactIDs
3✔
1061
}
1062

1063
// deployments
1064
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
2✔
1065
        ids := make([]string, len(devices))
2✔
1066
        for i, d := range devices {
4✔
1067
                ids[i] = d.ID
2✔
1068
        }
2✔
1069

1070
        return ids
2✔
1071
}
1072

1073
// updateDeploymentConstructor fills devices list with device ids
1074
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
1075
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
2✔
1076
        l := log.FromContext(ctx)
2✔
1077

2✔
1078
        id := identity.FromContext(ctx)
2✔
1079
        if id == nil {
2✔
1080
                l.Error("identity not present in the context")
×
1081
                return nil, ErrModelInternal
×
1082
        }
×
1083
        searchParams := model.SearchParams{
2✔
1084
                Page:    1,
2✔
1085
                PerPage: PerPageInventoryDevices,
2✔
1086
                Filters: []model.FilterPredicate{
2✔
1087
                        {
2✔
1088
                                Scope:     InventoryIdentityScope,
2✔
1089
                                Attribute: InventoryStatusAttributeName,
2✔
1090
                                Type:      "$eq",
2✔
1091
                                Value:     InventoryStatusAccepted,
2✔
1092
                        },
2✔
1093
                },
2✔
1094
        }
2✔
1095
        if len(constructor.Group) > 0 {
4✔
1096
                searchParams.Filters = append(
2✔
1097
                        searchParams.Filters,
2✔
1098
                        model.FilterPredicate{
2✔
1099
                                Scope:     InventoryGroupScope,
2✔
1100
                                Attribute: InventoryGroupAttributeName,
2✔
1101
                                Type:      "$eq",
2✔
1102
                                Value:     constructor.Group,
2✔
1103
                        })
2✔
1104
        }
2✔
1105

1106
        for {
4✔
1107
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
2✔
1108
                if err != nil {
3✔
1109
                        l.Errorf("error searching for devices")
1✔
1110
                        return nil, ErrModelInternal
1✔
1111
                }
1✔
1112
                if count < 1 {
3✔
1113
                        l.Errorf("no devices found")
1✔
1114
                        return nil, ErrNoDevices
1✔
1115
                }
1✔
1116
                if len(devices) < 1 {
2✔
1117
                        break
×
1118
                }
1119
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
2✔
1120
                if len(constructor.Devices) == count {
4✔
1121
                        break
2✔
1122
                }
1123
                searchParams.Page++
1✔
1124
        }
1125

1126
        return constructor, nil
2✔
1127
}
1128

1129
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1130
func (d *Deployments) CreateDeviceConfigurationDeployment(
1131
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1132
        deviceID, deploymentID string) (string, error) {
2✔
1133

2✔
1134
        if constructor == nil {
3✔
1135
                return "", ErrModelMissingInput
1✔
1136
        }
1✔
1137

1138
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
2✔
1139
                constructor,
2✔
1140
                deploymentID,
2✔
1141
        )
2✔
1142
        if err != nil {
2✔
1143
                return "", errors.Wrap(err, "failed to create deployment")
×
1144
        }
×
1145

1146
        deployment.DeviceList = []string{deviceID}
2✔
1147
        deployment.MaxDevices = 1
2✔
1148
        deployment.Configuration = []byte(constructor.Configuration)
2✔
1149
        deployment.Type = model.DeploymentTypeConfiguration
2✔
1150

2✔
1151
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
2✔
1152
        if err != nil {
3✔
1153
                return "", err
1✔
1154
        }
1✔
1155
        deployment.Groups = groups
2✔
1156

2✔
1157
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
4✔
1158
                if err == mongo.ErrConflictingDeployment {
3✔
1159
                        return "", ErrDuplicateDeployment
1✔
1160
                }
1✔
1161
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
3✔
1162
                        return "", ErrInvalidDeploymentID
1✔
1163
                }
1✔
1164
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1165
        }
1166

1167
        return deployment.Id, nil
2✔
1168
}
1169

1170
// CreateDeployment precomputes new deployment and schedules it for devices.
1171
func (d *Deployments) CreateDeployment(ctx context.Context,
1172
        constructor *model.DeploymentConstructor) (string, error) {
3✔
1173

3✔
1174
        var err error
3✔
1175

3✔
1176
        if constructor == nil {
4✔
1177
                return "", ErrModelMissingInput
1✔
1178
        }
1✔
1179

1180
        if err := constructor.Validate(); err != nil {
3✔
1181
                return "", errors.Wrap(err, "Validating deployment")
×
1182
        }
×
1183

1184
        if len(constructor.Group) > 0 || constructor.AllDevices {
5✔
1185
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
2✔
1186
                if err != nil {
3✔
1187
                        return "", err
1✔
1188
                }
1✔
1189
        }
1190

1191
        deployment, err := model.NewDeploymentFromConstructor(constructor)
3✔
1192
        if err != nil {
3✔
1193
                return "", errors.Wrap(err, "failed to create deployment")
×
1194
        }
×
1195

1196
        // Assign artifacts to the deployment.
1197
        // When new artifact(s) with the artifact name same as the one in the deployment
1198
        // will be uploaded to the backend, it will also become part of this deployment.
1199
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
3✔
1200
        if err != nil {
3✔
1201
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1202
        }
×
1203

1204
        if len(artifacts) == 0 {
4✔
1205
                return "", ErrNoArtifact
1✔
1206
        }
1✔
1207

1208
        deployment.Artifacts = getArtifactIDs(artifacts)
3✔
1209
        deployment.DeviceList = constructor.Devices
3✔
1210
        deployment.MaxDevices = len(constructor.Devices)
3✔
1211
        deployment.Type = model.DeploymentTypeSoftware
3✔
1212
        deployment.Filter = getDeploymentFilter(constructor)
3✔
1213
        if len(constructor.Group) > 0 {
5✔
1214
                deployment.Groups = []string{constructor.Group}
2✔
1215
        }
2✔
1216

1217
        // single device deployment case
1218
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
6✔
1219
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
3✔
1220
                if err != nil {
3✔
1221
                        return "", err
×
1222
                }
×
1223
                deployment.Groups = groups
3✔
1224
        }
1225

1226
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
5✔
1227
                if err == mongo.ErrConflictingDeployment {
4✔
1228
                        return "", ErrConflictingDeployment
2✔
1229
                }
2✔
1230
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1231
        }
1232

1233
        return deployment.Id, nil
3✔
1234
}
1235

1236
func (d *Deployments) getDeploymentGroups(
1237
        ctx context.Context,
1238
        devices []string,
1239
) ([]string, error) {
3✔
1240
        id := identity.FromContext(ctx)
3✔
1241

3✔
1242
        //only for single device deployment case
3✔
1243
        if len(devices) != 1 {
3✔
1244
                return nil, nil
×
1245
        }
×
1246

1247
        if id == nil {
4✔
1248
                id = &identity.Identity{}
1✔
1249
        }
1✔
1250

1251
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
3✔
1252
        if err != nil && err != inventory.ErrDevNotFound {
4✔
1253
                return nil, err
1✔
1254
        }
1✔
1255
        return groups, nil
3✔
1256
}
1257

1258
func getDeploymentFilter(
1259
        constructor *model.DeploymentConstructor,
1260
) *model.Filter {
3✔
1261

3✔
1262
        var filter *model.Filter
3✔
1263

3✔
1264
        if len(constructor.Group) > 0 {
5✔
1265
                filter = &model.Filter{
2✔
1266
                        Terms: []model.FilterPredicate{
2✔
1267
                                {
2✔
1268
                                        Scope:     InventoryGroupScope,
2✔
1269
                                        Attribute: InventoryGroupAttributeName,
2✔
1270
                                        Type:      "$eq",
2✔
1271
                                        Value:     constructor.Group,
2✔
1272
                                },
2✔
1273
                        },
2✔
1274
                }
2✔
1275
        } else if constructor.AllDevices {
7✔
1276
                filter = &model.Filter{
2✔
1277
                        Terms: []model.FilterPredicate{
2✔
1278
                                {
2✔
1279
                                        Scope:     InventoryIdentityScope,
2✔
1280
                                        Attribute: InventoryStatusAttributeName,
2✔
1281
                                        Type:      "$eq",
2✔
1282
                                        Value:     InventoryStatusAccepted,
2✔
1283
                                },
2✔
1284
                        },
2✔
1285
                }
2✔
1286
        } else if len(constructor.Devices) > 0 {
8✔
1287
                filter = &model.Filter{
3✔
1288
                        Terms: []model.FilterPredicate{
3✔
1289
                                {
3✔
1290
                                        Scope:     InventoryIdentityScope,
3✔
1291
                                        Attribute: InventoryIdAttributeName,
3✔
1292
                                        Type:      "$in",
3✔
1293
                                        Value:     constructor.Devices,
3✔
1294
                                },
3✔
1295
                        },
3✔
1296
                }
3✔
1297
        }
3✔
1298

1299
        return filter
3✔
1300
}
1301

1302
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1303
func (d *Deployments) IsDeploymentFinished(
1304
        ctx context.Context,
1305
        deploymentID string,
1306
) (bool, error) {
1✔
1307
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1308
        if err != nil {
1✔
1309
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1310
        }
×
1311
        if deployment == nil {
2✔
1312
                return true, nil
1✔
1313
        }
1✔
1314

1315
        return false, nil
1✔
1316
}
1317

1318
// GetDeployment fetches deployment by ID
1319
func (d *Deployments) GetDeployment(ctx context.Context,
1320
        deploymentID string) (*model.Deployment, error) {
2✔
1321

2✔
1322
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
2✔
1323
        if err != nil {
2✔
1324
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1325
        }
×
1326

1327
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
2✔
1328
                return nil, err
×
1329
        }
×
1330

1331
        return deployment, nil
2✔
1332
}
1333

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

2✔
1339
        var found bool
2✔
1340

2✔
1341
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
2✔
1342
        if err != nil {
3✔
1343
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1344
        }
1✔
1345

1346
        return found, nil
2✔
1347
}
1348

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

×
1353
        var found bool
×
1354

×
1355
        found, err := d.db.ExistByArtifactId(ctx, imageID)
×
1356
        if err != nil {
×
1357
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1358
        }
×
1359

1360
        return found, nil
×
1361
}
1362

1363
// Retrieves the model.Deployment and model.DeviceDeployment structures
1364
// for the device. Upon error, nil is returned for both deployment structures.
1365
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1366
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
3✔
1367

3✔
1368
        // Retrieve device deployment
3✔
1369
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
3✔
1370

3✔
1371
        if err != nil {
3✔
1372
                return nil, nil, errors.Wrap(err,
×
1373
                        "Searching for oldest active deployment for the device")
×
1374
        } else if deviceDeployment == nil {
5✔
1375
                return d.getNewDeploymentForDevice(ctx, deviceID)
2✔
1376
        }
2✔
1377

1378
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1379
        if err != nil {
2✔
1380
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1381
        }
×
1382
        if deployment == nil {
2✔
1383
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1384
        }
×
1385

1386
        return deployment, deviceDeployment, nil
2✔
1387
}
1388

1389
// getNewDeploymentForDevice returns deployment object and creates and returns
1390
// new device deployment for the device;
1391
//
1392
// we are interested only in the deployments that are newer than the latest
1393
// deployment applied by the device;
1394
// this way we guarantee that the device will not receive deployment
1395
// that is older than the one installed on the device;
1396
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1397
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1398

2✔
1399
        var lastDeployment *time.Time
2✔
1400
        //get latest device deployment for the device;
2✔
1401
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
2✔
1402
        if err != nil {
2✔
1403
                return nil, nil, errors.Wrap(err,
×
1404
                        "Searching for latest active deployment for the device")
×
1405
        } else if deviceDeployment == nil {
4✔
1406
                lastDeployment = &time.Time{}
2✔
1407
        } else {
4✔
1408
                lastDeployment = deviceDeployment.Created
2✔
1409
        }
2✔
1410

1411
        //get deployments newer then last device deployment
1412
        //iterate over deployments and check if the device is part of the deployment or not
1413
        var deploy *model.Deployment
2✔
1414
        for lastDeployment != nil {
4✔
1415
                deploy, err = d.db.FindNewerActiveDeployment(ctx, lastDeployment, deviceID)
2✔
1416
                if err != nil {
2✔
1417
                        return nil, nil, errors.Wrap(err, "Failed to search for newer active deployments")
×
1418
                }
×
1419
                if deploy != nil {
4✔
1420
                        if deploy.MaxDevices > 0 &&
2✔
1421
                                deploy.DeviceCount != nil &&
2✔
1422
                                *deploy.DeviceCount >= deploy.MaxDevices {
2✔
1423
                                lastDeployment = deploy.Created
×
1424
                                continue
×
1425
                        }
1426
                        deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
2✔
1427
                                deviceID, deploy, model.DeviceDeploymentStatusPending)
2✔
1428
                        if err != nil {
2✔
1429
                                return nil, nil, err
×
1430
                        }
×
1431
                        return deploy, deviceDeployment, nil
2✔
1432
                } else {
2✔
1433
                        lastDeployment = nil
2✔
1434
                }
2✔
1435
        }
1436
        return nil, nil, nil
2✔
1437
}
1438

1439
func (d *Deployments) createDeviceDeploymentWithStatus(
1440
        ctx context.Context, deviceID string,
1441
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1442
) (*model.DeviceDeployment, error) {
3✔
1443
        prevStatus := model.DeviceDeploymentStatusNull
3✔
1444
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
3✔
1445
        if err != nil && err != mongo.ErrStorageNotFound {
3✔
1446
                return nil, err
×
1447
        } else if deviceDeployment != nil {
3✔
1448
                prevStatus = deviceDeployment.Status
×
1449
        }
×
1450

1451
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
3✔
1452
        deviceDeployment.Status = status
3✔
1453
        deviceDeployment.Active = status.Active()
3✔
1454
        deviceDeployment.Created = deployment.Created
3✔
1455

3✔
1456
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
3✔
1457
                return nil, err
×
1458
        }
×
1459

1460
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
3✔
1461
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
3✔
1462
                return nil, err
×
1463
        }
×
1464

1465
        if prevStatus != status {
6✔
1466
                beforeStatus := deployment.GetStatus()
3✔
1467
                // after inserting new device deployment update deployment stats
3✔
1468
                // in the database, and update deployment status
3✔
1469
                deployment.Stats, err = d.db.UpdateStatsInc(
3✔
1470
                        ctx, deployment.Id,
3✔
1471
                        prevStatus, status,
3✔
1472
                )
3✔
1473
                if err != nil {
3✔
1474
                        return nil, err
×
1475
                }
×
1476
                newStatus := deployment.GetStatus()
3✔
1477
                if beforeStatus != newStatus {
3✔
1478
                        err = d.db.SetDeploymentStatus(
×
1479
                                ctx, deployment.Id,
×
1480
                                newStatus, time.Now(),
×
1481
                        )
×
1482
                        if err != nil {
×
1483
                                return nil, errors.Wrap(err,
×
1484
                                        "failed to update deployment status")
×
1485
                        }
×
1486
                }
1487
        }
1488

1489
        if !status.Active() {
4✔
1490
                err := d.reindexDevice(ctx, deviceID)
1✔
1491
                if err != nil {
1✔
1492
                        l := log.FromContext(ctx)
×
1493
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1494
                }
×
1495
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1496
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1497
                        l := log.FromContext(ctx)
×
1498
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1499
                }
×
1500
        }
1501

1502
        return deviceDeployment, nil
3✔
1503
}
1504

1505
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1506
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1507
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
3✔
1508

3✔
1509
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
3✔
1510
        if err != nil {
3✔
1511
                return nil, ErrModelInternal
×
1512
        } else if deployment == nil {
5✔
1513
                return nil, nil
2✔
1514
        }
2✔
1515

1516
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
3✔
1517
        if err != nil {
4✔
1518
                return nil, err
1✔
1519
        }
1✔
1520
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
3✔
1521
}
1522

1523
func (d *Deployments) getDeploymentInstructions(
1524
        ctx context.Context,
1525
        deployment *model.Deployment,
1526
        deviceDeployment *model.DeviceDeployment,
1527
        request *model.DeploymentNextRequest,
1528
) (*model.DeploymentInstructions, error) {
3✔
1529

3✔
1530
        var newArtifactAssigned bool
3✔
1531

3✔
1532
        l := log.FromContext(ctx)
3✔
1533

3✔
1534
        if deployment.Type == model.DeploymentTypeConfiguration {
4✔
1535
                // There's nothing more we need to do, the link must be filled
1✔
1536
                // in by the API layer.
1✔
1537
                return &model.DeploymentInstructions{
1✔
1538
                        ID: deployment.Id,
1✔
1539
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1540
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1541
                                // use deployment ID togheter with device ID as artifact ID
1✔
1542
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1543
                                ArtifactName:          deployment.ArtifactName,
1✔
1544
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1545
                        },
1✔
1546
                        Type: model.DeploymentTypeConfiguration,
1✔
1547
                }, nil
1✔
1548
        }
1✔
1549

1550
        // assing artifact to the device deployment
1551
        // only if it was not assgined previously
1552
        if deviceDeployment.Image == nil {
6✔
1553
                if err := d.assignArtifact(
3✔
1554
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
3✔
1555
                        return nil, err
×
1556
                }
×
1557
                newArtifactAssigned = true
3✔
1558
        }
1559

1560
        if deviceDeployment.Image == nil {
4✔
1561
                // No artifact - return empty response
1✔
1562
                return nil, nil
1✔
1563
        }
1✔
1564

1565
        // if the deployment is not forcing the installation, and
1566
        // if artifact was recognized as already installed, and this is
1567
        // a new device deployment - indicated by device deployment status "pending",
1568
        // handle already installed artifact case
1569
        if !deployment.ForceInstallation &&
3✔
1570
                d.isAlreadyInstalled(request, deviceDeployment) &&
3✔
1571
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
6✔
1572
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
3✔
1573
        }
3✔
1574

1575
        // if new artifact has been assigned to device deployment
1576
        // add artifact size to deployment total size,
1577
        // before returning deployment instruction to the device
1578
        if newArtifactAssigned {
4✔
1579
                if err := d.db.IncrementDeploymentTotalSize(
2✔
1580
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
2✔
1581
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1582
                }
×
1583
        }
1584

1585
        ctx, err := d.contextWithStorageSettings(ctx)
2✔
1586
        if err != nil {
2✔
1587
                return nil, err
×
1588
        }
×
1589

1590
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
2✔
1591
        link, err := d.objectStorage.GetRequest(
2✔
1592
                ctx,
2✔
1593
                imagePath,
2✔
1594
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
2✔
1595
                DefaultUpdateDownloadLinkExpire,
2✔
1596
                true,
2✔
1597
        )
2✔
1598
        if err != nil {
2✔
1599
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1600
        }
×
1601

1602
        instructions := &model.DeploymentInstructions{
2✔
1603
                ID: deviceDeployment.DeploymentId,
2✔
1604
                Artifact: model.ArtifactDeploymentInstructions{
2✔
1605
                        ID: deviceDeployment.Image.Id,
2✔
1606
                        ArtifactName: deviceDeployment.Image.
2✔
1607
                                ArtifactMeta.Name,
2✔
1608
                        Source: *link,
2✔
1609
                        DeviceTypesCompatible: deviceDeployment.Image.
2✔
1610
                                ArtifactMeta.DeviceTypesCompatible,
2✔
1611
                },
2✔
1612
        }
2✔
1613

2✔
1614
        return instructions, nil
2✔
1615
}
1616

1617
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1618
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
3✔
1619
        if deviceDeployment.Request != nil {
4✔
1620
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1621
                        // the device reported different device type and/or artifact name during the
1✔
1622
                        // update process, this can happen if the mender-store DB in the client is not
1✔
1623
                        // persistent so a new deployment start without a previous one is still ongoing;
1✔
1624
                        // mark deployment for this device as failed to force client to rollback
1✔
1625
                        l := log.FromContext(ctx)
1✔
1626
                        l.Errorf(
1✔
1627
                                "Device with id %s reported new data: %s during update process;"+
1✔
1628
                                        "old data: %s",
1✔
1629
                                deviceID, request, deviceDeployment.Request)
1✔
1630

1✔
1631
                        if err := d.updateDeviceDeploymentStatus(ctx, deviceDeployment,
1✔
1632
                                model.DeviceDeploymentState{
1✔
1633
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1634
                                }); err != nil {
1✔
1635
                                return errors.Wrap(err, "Failed to update deployment status")
×
1636
                        }
×
1637
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1638
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1639
                        }
×
1640
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1641
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1642
                                l := log.FromContext(ctx)
×
1643
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1644
                        }
×
1645
                        return ErrConflictingRequestData
1✔
1646
                }
1647
        } else {
3✔
1648
                // save the request
3✔
1649
                if err := d.db.SaveDeviceDeploymentRequest(
3✔
1650
                        ctx, deviceDeployment.Id, request); err != nil {
3✔
1651
                        return err
×
1652
                }
×
1653
        }
1654
        return nil
3✔
1655
}
1656

1657
// updateDeviceDeploymentStatus will update the deployment status for device of
1658
// ID `deviceID`. Returns nil if update was successful.
1659
func (d *Deployments) UpdateDeviceDeploymentStatus(
1660
        ctx context.Context,
1661
        deviceID, deploymentID string,
1662
        ddState model.DeviceDeploymentState,
1663
) error {
3✔
1664
        deviceDeployment, err := d.db.GetDeviceDeployment(
3✔
1665
                ctx, deviceID, deploymentID, false,
3✔
1666
        )
3✔
1667
        if err == mongo.ErrStorageNotFound {
5✔
1668
                return ErrStorageNotFound
2✔
1669
        } else if err != nil {
5✔
1670
                return err
×
1671
        }
×
1672

1673
        return d.updateDeviceDeploymentStatus(ctx, deviceDeployment, ddState)
3✔
1674
}
1675

1676
func (d *Deployments) updateDeviceDeploymentStatus(
1677
        ctx context.Context,
1678
        dd *model.DeviceDeployment,
1679
        ddState model.DeviceDeploymentState,
1680
) error {
3✔
1681

3✔
1682
        l := log.FromContext(ctx)
3✔
1683

3✔
1684
        l.Infof("New status: %s for device %s deployment: %v",
3✔
1685
                ddState.Status, dd.DeviceId, dd.DeploymentId,
3✔
1686
        )
3✔
1687

3✔
1688
        var finishTime *time.Time = nil
3✔
1689
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
6✔
1690
                now := time.Now()
3✔
1691
                finishTime = &now
3✔
1692
        }
3✔
1693

1694
        currentStatus := dd.Status
3✔
1695

3✔
1696
        if currentStatus == model.DeviceDeploymentStatusAborted {
3✔
1697
                return ErrDeploymentAborted
×
1698
        }
×
1699

1700
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
3✔
1701
                return ErrDeviceDecommissioned
×
1702
        }
×
1703

1704
        // nothing to do
1705
        if ddState.Status == currentStatus {
3✔
1706
                return nil
×
1707
        }
×
1708

1709
        // update finish time
1710
        ddState.FinishTime = finishTime
3✔
1711

3✔
1712
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
3✔
1713
                dd.DeviceId, dd.DeploymentId, ddState, dd.Status)
3✔
1714
        if err != nil {
3✔
1715
                return err
×
1716
        }
×
1717

1718
        if old != ddState.Status {
6✔
1719
                // fetch deployment stats and update deployment status
3✔
1720
                deployment, err := d.db.FindDeploymentByID(ctx, dd.DeploymentId)
3✔
1721
                if err != nil {
3✔
1722
                        return errors.Wrap(err, "failed when searching for deployment")
×
1723
                }
×
1724
                beforeStatus := deployment.GetStatus()
3✔
1725

3✔
1726
                deployment.Stats, err = d.db.UpdateStatsInc(ctx, dd.DeploymentId, old, ddState.Status)
3✔
1727
                if err != nil {
3✔
UNCOV
1728
                        return err
×
UNCOV
1729
                }
×
1730
                newStatus := deployment.GetStatus()
3✔
1731
                if beforeStatus != newStatus {
6✔
1732
                        err = d.db.SetDeploymentStatus(ctx, dd.DeploymentId, newStatus, time.Now())
3✔
1733
                        if err != nil {
3✔
UNCOV
1734
                                return errors.Wrap(err, "failed to update deployment status")
×
UNCOV
1735
                        }
×
1736
                }
1737
        }
1738

1739
        if !ddState.Status.Active() {
6✔
1740
                l := log.FromContext(ctx)
3✔
1741
                ldd := model.DeviceDeployment{
3✔
1742
                        DeviceId:     dd.DeviceId,
3✔
1743
                        DeploymentId: dd.DeploymentId,
3✔
1744
                        Id:           dd.Id,
3✔
1745
                        Status:       ddState.Status,
3✔
1746
                }
3✔
1747
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
3✔
UNCOV
1748
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
UNCOV
1749
                }
×
1750
                if err := d.reindexDevice(ctx, dd.DeviceId); err != nil {
3✔
1751
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1752
                }
×
1753
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
3✔
1754
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1755
                }
×
1756
        }
1757

1758
        return nil
3✔
1759
}
1760

1761
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1762
        deploymentID string) (model.Stats, error) {
1✔
1763

1✔
1764
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1765

1✔
1766
        if err != nil {
1✔
UNCOV
1767
                return nil, errors.Wrap(err, "checking deployment id")
×
UNCOV
1768
        }
×
1769

1770
        if deployment == nil {
1✔
1771
                return nil, nil
×
UNCOV
1772
        }
×
1773

1774
        return deployment.Stats, nil
1✔
1775
}
1776
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
UNCOV
1777
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
UNCOV
1778

×
UNCOV
1779
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1780

×
1781
        if err != nil {
×
1782
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1783
        }
×
1784

1785
        if deploymentStats == nil {
×
1786
                return nil, ErrModelDeploymentNotFound
×
UNCOV
1787
        }
×
1788

1789
        return deploymentStats, nil
×
1790
}
1791

1792
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1793
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1794
        deploymentID string) ([]model.DeviceDeployment, error) {
2✔
1795

2✔
1796
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
2✔
1797
        if err != nil {
2✔
UNCOV
1798
                return nil, ErrModelInternal
×
UNCOV
1799
        }
×
1800

1801
        if deployment == nil {
2✔
1802
                return nil, ErrModelDeploymentNotFound
×
UNCOV
1803
        }
×
1804

1805
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
2✔
1806
        if err != nil {
2✔
UNCOV
1807
                return nil, ErrModelInternal
×
UNCOV
1808
        }
×
1809

1810
        return statuses, nil
2✔
1811
}
1812

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

1✔
1816
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1817
        if err != nil {
1✔
UNCOV
1818
                return nil, -1, ErrModelInternal
×
UNCOV
1819
        }
×
1820

1821
        if deployment == nil {
1✔
1822
                return nil, -1, ErrModelDeploymentNotFound
×
UNCOV
1823
        }
×
1824

1825
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1826
        if err != nil {
1✔
UNCOV
1827
                return nil, -1, ErrModelInternal
×
UNCOV
1828
        }
×
1829

1830
        return statuses, totalCount, nil
1✔
1831
}
1832

1833
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1834
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
2✔
1835
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
2✔
1836
        if err != nil {
3✔
1837
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1838
        }
1✔
1839

1840
        deploymentIDs := make([]string, len(deviceDeployments))
2✔
1841
        for i, deviceDeployment := range deviceDeployments {
4✔
1842
                deploymentIDs[i] = deviceDeployment.DeploymentId
2✔
1843
        }
2✔
1844
        var deployments []*model.Deployment
2✔
1845
        if len(deviceDeployments) > 0 {
4✔
1846
                deployments, _, err = d.db.FindDeployments(ctx, model.Query{
2✔
1847
                        IDs:          deploymentIDs,
2✔
1848
                        Limit:        len(deviceDeployments),
2✔
1849
                        DisableCount: true,
2✔
1850
                })
2✔
1851
                if err != nil {
3✔
1852
                        return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1853
                }
1✔
1854
        }
1855

1856
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1857
        for _, deployment := range deployments {
4✔
1858
                deploymentsMap[deployment.Id] = deployment
2✔
1859
        }
2✔
1860

1861
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1862
        for i, deviceDeployment := range deviceDeployments {
4✔
1863
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
4✔
1864
                        res = append(res, model.DeviceDeploymentListItem{
2✔
1865
                                Id:         deviceDeployment.Id,
2✔
1866
                                Deployment: deployment,
2✔
1867
                                Device:     &deviceDeployments[i],
2✔
1868
                        })
2✔
1869
                } else {
3✔
1870
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1871
                                Id:     deviceDeployment.Id,
1✔
1872
                                Device: &deviceDeployments[i],
1✔
1873
                        })
1✔
1874
                }
1✔
1875
        }
1876

1877
        return res, totalCount, nil
2✔
1878
}
1879

1880
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1881
        ctx context.Context,
1882
        deployment *model.Deployment,
1883
) error {
3✔
1884
        if deployment.DeviceCount == nil {
3✔
UNCOV
1885
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
UNCOV
1886
                if err != nil {
×
UNCOV
1887
                        return errors.Wrap(err, "counting device deployments")
×
1888
                }
×
1889
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1890
                if err != nil {
×
1891
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1892
                }
×
1893
                deployment.DeviceCount = &deviceCount
×
1894
        }
1895

1896
        return nil
3✔
1897
}
1898

1899
func (d *Deployments) LookupDeployment(ctx context.Context,
1900
        query model.Query) ([]*model.Deployment, int64, error) {
3✔
1901
        list, totalCount, err := d.db.FindDeployments(ctx, query)
3✔
1902

3✔
1903
        if err != nil {
4✔
1904
                return nil, 0, errors.Wrap(err, "searching for deployments")
1✔
1905
        }
1✔
1906

1907
        if list == nil {
6✔
1908
                return make([]*model.Deployment, 0), 0, nil
3✔
1909
        }
3✔
1910

1911
        for _, deployment := range list {
4✔
1912
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
2✔
UNCOV
1913
                        return nil, 0, err
×
UNCOV
1914
                }
×
1915
        }
1916

1917
        return list, totalCount, nil
2✔
1918
}
1919

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

1✔
1925
        // repack to temporary deployment log and validate
1✔
1926
        dlog := model.DeploymentLog{
1✔
1927
                DeviceID:     deviceID,
1✔
1928
                DeploymentID: deploymentID,
1✔
1929
                Messages:     logs,
1✔
1930
        }
1✔
1931
        if err := dlog.Validate(); err != nil {
1✔
UNCOV
1932
                return errors.Wrap(err, ErrStorageInvalidLog.Error())
×
UNCOV
1933
        }
×
1934

1935
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1936
                if err != nil {
×
UNCOV
1937
                        return err
×
UNCOV
1938
                } else {
×
1939
                        return ErrModelDeploymentNotFound
×
1940
                }
×
1941
        }
1942

1943
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
UNCOV
1944
                return err
×
UNCOV
1945
        }
×
1946

1947
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1948
                deviceID, deploymentID, true)
1✔
1949
}
1950

1951
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1952
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1953

1✔
1954
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1955
                deviceID, deploymentID)
1✔
1956
}
1✔
1957

1958
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1959
        deploymentID string, deviceID string) (bool, error) {
1✔
1960
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1961
}
1✔
1962

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

2✔
1966
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
3✔
1967
                return err
1✔
1968
        }
1✔
1969

1970
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
2✔
1971
                ctx, deploymentID)
2✔
1972
        if err != nil {
3✔
1973
                return err
1✔
1974
        }
1✔
1975

1976
        // update statistics
1977
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
3✔
1978
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1979
        }
1✔
1980

1981
        // when aborting the deployment we need to set status directly instead of
1982
        // using recalcDeploymentStatus method;
1983
        // it is possible that the deployment does not have any device deployments yet;
1984
        // in that case, all statistics are 0 and calculating status based on statistics
1985
        // will not work - the calculated status will be "pending"
1986
        if err := d.db.SetDeploymentStatus(ctx,
2✔
1987
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
2✔
UNCOV
1988
                return errors.Wrap(err, "failed to update deployment status")
×
UNCOV
1989
        }
×
1990

1991
        return nil
2✔
1992
}
1993

1994
func (d *Deployments) updateDeviceDeploymentsStatus(
1995
        ctx context.Context,
1996
        deviceId string,
1997
        status model.DeviceDeploymentStatus,
1998
) error {
2✔
1999
        var latestDeployment *time.Time
2✔
2000
        // Retrieve active device deployment for the device
2✔
2001
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
2✔
2002
        if err != nil {
3✔
2003
                return errors.Wrap(err, "Searching for active deployment for the device")
1✔
2004
        } else if deviceDeployment != nil {
4✔
2005
                now := time.Now()
1✔
2006
                ddStatus := model.DeviceDeploymentState{
1✔
2007
                        Status:     status,
1✔
2008
                        FinishTime: &now,
1✔
2009
                }
1✔
2010
                if err := d.updateDeviceDeploymentStatus(
1✔
2011
                        ctx, deviceDeployment, ddStatus,
1✔
2012
                ); err != nil {
1✔
UNCOV
2013
                        return errors.Wrap(err, "updating device deployment status")
×
UNCOV
2014
                }
×
2015
                latestDeployment = deviceDeployment.Created
1✔
2016
        } else {
2✔
2017
                // get latest device deployment for the device
2✔
2018
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
2✔
2019
                if err != nil {
2✔
UNCOV
2020
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
2021
                } else if deviceDeployment == nil {
4✔
2022
                        latestDeployment = &time.Time{}
2✔
2023
                } else {
3✔
2024
                        latestDeployment = deviceDeployment.Created
1✔
2025
                }
1✔
2026
        }
2027

2028
        // get deployments newer then last device deployment
2029
        // iterate over deployments and check if the device is part of the deployment or not
2030
        // if the device is part of the deployment create new, decommisioned device deployment
2031
        var deploy *model.Deployment
2✔
2032
        deploy, err = d.db.FindNewerActiveDeployment(ctx, latestDeployment, deviceId)
2✔
2033
        if err != nil {
2✔
UNCOV
2034
                return errors.Wrap(err, "Failed to search for newer active deployments")
×
UNCOV
2035
        }
×
2036
        if deploy != nil {
3✔
2037
                deviceDeployment, err = d.createDeviceDeploymentWithStatus(ctx,
1✔
2038
                        deviceId, deploy, status)
1✔
2039
                if err != nil {
1✔
UNCOV
2040
                        return err
×
UNCOV
2041
                }
×
2042
                if !status.Active() {
2✔
2043
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
2044
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
UNCOV
2045
                                l := log.FromContext(ctx)
×
UNCOV
2046
                                l.Warn(errors.Wrap(err, "failed to trigger a deployment reindex"))
×
UNCOV
2047
                        }
×
2048
                }
2049
        }
2050

2051
        if err := d.reindexDevice(ctx, deviceId); err != nil {
2✔
UNCOV
2052
                l := log.FromContext(ctx)
×
UNCOV
2053
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
2054
        }
×
2055

2056
        return nil
2✔
2057
}
2058

2059
// DecommissionDevice updates the status of all the pending and active deployments for a device
2060
// to decommissioned
2061
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
2✔
2062
        return d.updateDeviceDeploymentsStatus(
2✔
2063
                ctx,
2✔
2064
                deviceId,
2✔
2065
                model.DeviceDeploymentStatusDecommissioned,
2✔
2066
        )
2✔
2067
}
2✔
2068

2069
// AbortDeviceDeployments aborts all the pending and active deployments for a device
2070
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
1✔
2071
        return d.updateDeviceDeploymentsStatus(
1✔
2072
                ctx,
1✔
2073
                deviceId,
1✔
2074
                model.DeviceDeploymentStatusAborted,
1✔
2075
        )
1✔
2076
}
1✔
2077

2078
// DeleteDeviceDeploymentsHistory deletes the device deployments history
2079
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
1✔
2080
        // get device deployments which will be marked as deleted
1✔
2081
        f := false
1✔
2082
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
1✔
2083
        if err != nil {
1✔
UNCOV
2084
                return err
×
UNCOV
2085
        }
×
2086

2087
        // no device deployments to update
2088
        if len(dd) <= 0 {
1✔
UNCOV
2089
                return nil
×
UNCOV
2090
        }
×
2091

2092
        // mark device deployments as deleted
2093
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
2✔
2094
                return err
1✔
2095
        }
1✔
2096

2097
        // trigger reindexing of updated device deployments
2098
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2099
        for i, d := range dd {
2✔
2100
                deviceDeployments[i].ID = d.Id
1✔
2101
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2102
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2103
        }
1✔
2104
        if d.reportingClient != nil {
2✔
2105
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2106
        }
1✔
2107
        return err
1✔
2108
}
2109

2110
// Storage settings
2111
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
2112
        settings, err := d.db.GetStorageSettings(ctx)
3✔
2113
        if err != nil {
4✔
2114
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2115
        }
1✔
2116

2117
        return settings, nil
3✔
2118
}
2119

2120
func (d *Deployments) SetStorageSettings(
2121
        ctx context.Context,
2122
        storageSettings *model.StorageSettings,
2123
) error {
2✔
2124
        if storageSettings != nil {
4✔
2125
                ctx = storage.SettingsWithContext(ctx, storageSettings)
2✔
2126
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
2✔
UNCOV
2127
                        return errors.WithMessage(err,
×
UNCOV
2128
                                "the provided storage settings failed the health check",
×
UNCOV
2129
                        )
×
2130
                }
×
2131
        }
2132
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
3✔
2133
                return errors.Wrap(err, "Failed to save settings")
1✔
2134
        }
1✔
2135

2136
        return nil
2✔
2137
}
2138

2139
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
1✔
2140
        d.reportingClient = c
1✔
2141
        return d
1✔
2142
}
1✔
2143

2144
func (d *Deployments) haveReporting() bool {
2✔
2145
        return d.reportingClient != nil
2✔
2146
}
2✔
2147

2148
func (d *Deployments) search(
2149
        ctx context.Context,
2150
        tid string,
2151
        parms model.SearchParams,
2152
) ([]model.InvDevice, int, error) {
2✔
2153
        if d.haveReporting() {
3✔
2154
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2155
        } else {
3✔
2156
                return d.inventoryClient.Search(ctx, tid, parms)
2✔
2157
        }
2✔
2158
}
2159

2160
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2161
        ctx context.Context,
2162
        artifactName string,
2163
) error {
3✔
2164
        // first check if there are pending deployments with given artifact name
3✔
2165
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
3✔
2166
        if err != nil {
3✔
UNCOV
2167
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
UNCOV
2168
        }
×
2169
        if !exists {
5✔
2170
                return nil
2✔
2171
        }
2✔
2172

2173
        // Assign artifacts to the deployments with given artifact name
2174
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2175
        if err != nil {
1✔
UNCOV
2176
                return errors.Wrap(err, "Finding artifact with given name")
×
UNCOV
2177
        }
×
2178

2179
        if len(artifacts) == 0 {
1✔
2180
                return ErrNoArtifact
×
UNCOV
2181
        }
×
2182
        artifactIDs := getArtifactIDs(artifacts)
1✔
2183
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2184
}
2185

2186
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
3✔
2187
        if d.reportingClient != nil {
4✔
2188
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
1✔
2189
        }
1✔
2190
        return nil
3✔
2191
}
2192

2193
func (d *Deployments) reindexDeployment(ctx context.Context,
2194
        deviceID, deploymentID, ID string) error {
3✔
2195
        if d.reportingClient != nil {
4✔
2196
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
1✔
2197
        }
1✔
2198
        return nil
3✔
2199
}
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