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

mendersoftware / deployments / 876611228

pending completion
876611228

push

gitlab-ci

GitHub
Merge pull request #864 from merlin-northern/men_6474_skip_verify_flag

65 of 70 new or added lines in 4 files covered. (92.86%)

512 existing lines in 6 files now uncovered.

7094 of 8937 relevant lines covered (79.38%)

69.35 hits per line

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

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

15
package app
16

17
import (
18
        "bytes"
19
        "context"
20
        "encoding/json"
21
        "io"
22
        "path"
23
        "reflect"
24
        "strings"
25
        "time"
26

27
        "github.com/google/uuid"
28
        "github.com/pkg/errors"
29

30
        "github.com/mendersoftware/go-lib-micro/identity"
31
        "github.com/mendersoftware/go-lib-micro/log"
32
        "github.com/mendersoftware/mender-artifact/areader"
33
        "github.com/mendersoftware/mender-artifact/artifact"
34
        "github.com/mendersoftware/mender-artifact/awriter"
35
        "github.com/mendersoftware/mender-artifact/handlers"
36

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

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

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

61
        fileSuffixTmp = ".tmp"
62

63
        inprogressIdleTime = time.Hour
64
)
65

66
var (
67
        ArtifactConfigureType = "mender-configure"
68
)
69

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

86
        ErrMsgArtifactConflict = "An artifact with the same name has conflicting dependencies"
87

88
        // deployments
89
        ErrModelMissingInput       = errors.New("Missing input deployment data")
90
        ErrModelInvalidDeviceID    = errors.New("Invalid device ID")
91
        ErrModelDeploymentNotFound = errors.New("Deployment not found")
92
        ErrModelInternal           = errors.New("Internal error")
93
        ErrStorageInvalidLog       = errors.New("Invalid deployment log")
94
        ErrStorageNotFound         = errors.New("Not found")
95
        ErrDeploymentAborted       = errors.New("Deployment aborted")
96
        ErrDeviceDecommissioned    = errors.New("Device decommissioned")
97
        ErrNoArtifact              = errors.New("No artifact for the deployment")
98
        ErrNoDevices               = errors.New("No devices for the deployment")
99
        ErrDuplicateDeployment     = errors.New("Deployment with given ID already exists")
100
        ErrInvalidDeploymentID     = errors.New("Deployment ID must be a valid UUID")
101
        ErrConflictingRequestData  = errors.New("Device provided conflicting request data")
102
)
103

104
//deployments
105

106
//go:generate ../utils/mockgen.sh
107
type App interface {
108
        HealthCheck(ctx context.Context) error
109
        // limits
110
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
111
        ProvisionTenant(ctx context.Context, tenant_id string) error
112

113
        // Storage Settings
114
        GetStorageSettings(ctx context.Context) (*model.StorageSettings, error)
115
        SetStorageSettings(ctx context.Context, storageSettings *model.StorageSettings) error
116

117
        // images
118
        ListImages(
119
                ctx context.Context,
120
                filters *model.ReleaseOrImageFilter,
121
        ) ([]*model.Image, int, error)
122
        DownloadLink(ctx context.Context, imageID string,
123
                expire time.Duration) (*model.Link, error)
124
        UploadLink(
125
                ctx context.Context,
126
                expire time.Duration,
127
                skipVerify bool,
128
        ) (*model.UploadLink, error)
129
        CompleteUpload(ctx context.Context, intentID string, skipVerify bool) error
130
        GetImage(ctx context.Context, id string) (*model.Image, error)
131
        DeleteImage(ctx context.Context, imageID string) error
132
        CreateImage(ctx context.Context,
133
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
134
        GenerateImage(ctx context.Context,
135
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
136
        GenerateConfigurationImage(
137
                ctx context.Context,
138
                deviceType string,
139
                deploymentID string,
140
        ) (io.Reader, error)
141
        EditImage(ctx context.Context, id string,
142
                constructorData *model.ImageMeta) (bool, error)
143

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

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

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

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

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

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

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

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

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

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

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

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

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

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

291
        return nil
1✔
292
}
293

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

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

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

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

1✔
319
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
1✔
320

1✔
321
        tee := io.TeeReader(artifactReader, pW)
1✔
322

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

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

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

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

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

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

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

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

413
        return artifactID, nil
1✔
414
}
415

416
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
417
// creates image structure in the system, and starts the workflow to generate the
418
// artifact from them.
419
// Returns image ID and nil on success.
420
func (d *Deployments) GenerateImage(ctx context.Context,
421
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
1✔
422

1✔
423
        if multipartGenerateImageMsg == nil {
1✔
424
                return "", ErrModelMultipartUploadMsgMalformed
425
        }
426

427
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
1✔
428
        if err != nil {
1✔
429
                return "", err
430
        }
431
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
1✔
432
                multipartGenerateImageMsg.TenantID = id.Tenant
433
        }
434
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
1✔
435
        if err != nil {
1✔
436
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
437
                        return "", errors.Wrap(err, cleanupErr.Error())
438
                }
439
                return "", err
440
        }
441

442
        return multipartGenerateImageMsg.ArtifactID, err
1✔
443
}
444

445
func (d *Deployments) GenerateConfigurationImage(
446
        ctx context.Context,
447
        deviceType string,
448
        deploymentID string,
449
) (io.Reader, error) {
1✔
450
        var buf bytes.Buffer
1✔
451
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
452
        if err != nil {
1✔
453
                return nil, err
454
        } else if dpl == nil {
1✔
455
                return nil, ErrModelDeploymentNotFound
456
        }
457
        var metaData map[string]interface{}
1✔
458
        err = json.Unmarshal(dpl.Configuration, &metaData)
1✔
459
        if err != nil {
1✔
460
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
461
        }
462

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

1✔
488
        return &buf, err
1✔
489
}
490

491
// handleRawFile parses raw data, uploads it to the file storage,
492
// and starts the workflow to generate the artifact.
493
// Returns the object path to the file and nil on success.
494
func (d *Deployments) handleRawFile(ctx context.Context,
495
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
1✔
496
        l := log.FromContext(ctx)
1✔
497
        uid, _ := uuid.NewRandom()
1✔
498
        artifactID := uid.String()
1✔
499
        multipartMsg.ArtifactID = artifactID
1✔
500
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
1✔
501

1✔
502
        // check if artifact is unique
1✔
503
        // artifact is considered to be unique if there is no artifact with the same name
1✔
504
        // and supporting the same platform in the system
1✔
505
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
1✔
506
                multipartMsg.Name,
1✔
507
                multipartMsg.DeviceTypesCompatible,
1✔
508
        )
1✔
509
        if err != nil {
1✔
510
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
511
        }
512
        if !isArtifactUnique {
1✔
513
                return "", ErrModelArtifactNotUnique
514
        }
515

516
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
517
        if err != nil {
1✔
UNCOV
518
                return "", err
UNCOV
519
        }
520
        err = d.objectStorage.PutObject(
1✔
521
                ctx, filePath, multipartMsg.FileReader,
1✔
522
        )
1✔
523
        if err != nil {
1✔
524
                return "", err
525
        }
526
        defer func() {
2✔
527
                if err != nil {
1✔
528
                        e := d.objectStorage.DeleteObject(ctx, filePath)
529
                        if e != nil {
530
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
531
                                        filePath, e)
532
                        }
533
                }
534
        }()
535

536
        link, err := d.objectStorage.GetRequest(
1✔
537
                ctx,
1✔
538
                filePath,
1✔
539
                path.Base(filePath),
1✔
540
                DefaultImageGenerationLinkExpire,
1✔
541
        )
1✔
542
        if err != nil {
1✔
543
                return "", err
544
        }
545
        multipartMsg.GetArtifactURI = link.Uri
1✔
546

1✔
547
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
1✔
548
        if err != nil {
1✔
549
                return "", err
550
        }
551
        multipartMsg.DeleteArtifactURI = link.Uri
1✔
552

1✔
553
        return artifactID, nil
1✔
554
}
555

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

1✔
560
        image, err := d.db.FindImageByID(ctx, id)
1✔
561
        if err != nil {
1✔
UNCOV
562
                return nil, errors.Wrap(err, "Searching for image with specified ID")
UNCOV
563
        }
564

565
        if image == nil {
2✔
566
                return nil, nil
1✔
567
        }
1✔
568

569
        return image, nil
1✔
570
}
571

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

1✔
582
        if err != nil {
1✔
UNCOV
583
                return errors.Wrap(err, "Getting image metadata")
UNCOV
584
        }
585

586
        if found == nil {
1✔
UNCOV
587
                return ErrImageMetaNotFound
UNCOV
588
        }
589

590
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
591
        if err != nil {
1✔
UNCOV
592
                return errors.Wrap(err, "Checking if image is used in active deployment")
UNCOV
593
        }
594

595
        // Image is in use, not allowed to delete
596
        if inUse {
2✔
597
                return ErrModelImageInActiveDeployment
1✔
598
        }
1✔
599

600
        // Delete image file (call to external service)
601
        // Noop for not existing file
602
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
603
        if err != nil {
1✔
UNCOV
604
                return err
605
        }
606
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
607
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
UNCOV
608
                return errors.Wrap(err, "Deleting image file")
UNCOV
609
        }
610

611
        // Delete metadata
612
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
UNCOV
613
                return errors.Wrap(err, "Deleting image metadata")
UNCOV
614
        }
615

616
        return nil
1✔
617
}
618

619
// ListImages according to specified filers.
620
func (d *Deployments) ListImages(
621
        ctx context.Context,
622
        filters *model.ReleaseOrImageFilter,
623
) ([]*model.Image, int, error) {
1✔
624
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
625
        if err != nil {
1✔
626
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
627
        }
628

629
        if imageList == nil {
2✔
630
                return make([]*model.Image, 0), 0, nil
1✔
631
        }
1✔
632

633
        return imageList, count, nil
1✔
634
}
635

636
// EditObject allows editing only if image have not been used yet in any deployment.
637
func (d *Deployments) EditImage(ctx context.Context, imageID string,
UNCOV
638
        constructor *model.ImageMeta) (bool, error) {
639

640
        if err := constructor.Validate(); err != nil {
UNCOV
641
                return false, errors.Wrap(err, "Validating image metadata")
UNCOV
642
        }
643

UNCOV
644
        found, err := d.ImageUsedInDeployment(ctx, imageID)
UNCOV
645
        if err != nil {
UNCOV
646
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
UNCOV
647
        }
648

UNCOV
649
        if found {
UNCOV
650
                return false, ErrModelImageUsedInAnyDeployment
651
        }
652

653
        foundImage, err := d.db.FindImageByID(ctx, imageID)
654
        if err != nil {
655
                return false, errors.Wrap(err, "Searching for image with specified ID")
UNCOV
656
        }
657

658
        if foundImage == nil {
659
                return false, nil
660
        }
661

662
        foundImage.SetModified(time.Now())
663
        foundImage.ImageMeta = constructor
664

UNCOV
665
        _, err = d.db.Update(ctx, foundImage)
666
        if err != nil {
667
                return false, errors.Wrap(err, "Updating image matadata")
668
        }
669

UNCOV
670
        return true, nil
671
}
672

673
// DownloadLink presigned GET link to download image file.
674
// Returns error if image have not been uploaded.
675
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
676
        expire time.Duration) (*model.Link, error) {
1✔
677

1✔
678
        image, err := d.GetImage(ctx, imageID)
1✔
679
        if err != nil {
1✔
680
                return nil, errors.Wrap(err, "Searching for image with specified ID")
681
        }
682

683
        if image == nil {
1✔
UNCOV
684
                return nil, nil
UNCOV
685
        }
686

687
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
688
        if err != nil {
1✔
UNCOV
689
                return nil, err
UNCOV
690
        }
691
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
692
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
693
        if err != nil {
1✔
694
                return nil, errors.Wrap(err, "Searching for image file")
UNCOV
695
        }
696

697
        link, err := d.objectStorage.GetRequest(
1✔
698
                ctx,
1✔
699
                imagePath,
1✔
700
                image.Name+model.ArtifactFileSuffix,
1✔
701
                expire,
1✔
702
        )
1✔
703
        if err != nil {
1✔
UNCOV
704
                return nil, errors.Wrap(err, "Generating download link")
UNCOV
705
        }
706

707
        return link, nil
1✔
708
}
709

710
func (d *Deployments) UploadLink(
711
        ctx context.Context,
712
        expire time.Duration,
713
        skipVerify bool,
714
) (*model.UploadLink, error) {
1✔
715
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
716
        if err != nil {
1✔
717
                return nil, err
718
        }
719

720
        artifactID := uuid.New().String()
1✔
721
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
1✔
722
        if skipVerify {
2✔
723
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
724
        }
1✔
725
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
1✔
726
        if err != nil {
1✔
727
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
728
        }
729
        upLink := &model.UploadLink{
1✔
730
                ArtifactID: artifactID,
1✔
731
                IssuedAt:   time.Now(),
1✔
732
                Link:       *link,
1✔
733
        }
1✔
734
        err = d.db.InsertUploadIntent(ctx, upLink)
1✔
735
        if err != nil {
1✔
736
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
737
        }
738

739
        return upLink, err
1✔
740
}
741

742
func (d *Deployments) processUploadedArtifact(
743
        ctx context.Context,
744
        artifactID string,
745
        artifact io.ReadCloser,
746
        skipVerify bool,
747
) error {
1✔
748
        linkStatus := model.LinkStatusCompleted
1✔
749

1✔
750
        l := log.FromContext(ctx)
1✔
751
        defer artifact.Close()
1✔
752
        ctx, cancel := context.WithCancel(ctx)
1✔
753
        defer cancel()
1✔
754
        go func() { // Heatbeat routine
2✔
755
                ticker := time.NewTicker(inprogressIdleTime / 2)
1✔
756
                done := ctx.Done()
1✔
757
                defer ticker.Stop()
1✔
758
                for {
2✔
759
                        select {
1✔
UNCOV
760
                        case <-ticker.C:
UNCOV
761
                                err := d.db.UpdateUploadIntentStatus(
UNCOV
762
                                        ctx,
UNCOV
763
                                        artifactID,
UNCOV
764
                                        model.LinkStatusProcessing,
UNCOV
765
                                        model.LinkStatusProcessing,
UNCOV
766
                                )
UNCOV
767
                                if err != nil {
UNCOV
768
                                        l.Errorf("failed to update upload link timestamp: %s", err)
UNCOV
769
                                        cancel()
UNCOV
770
                                        return
UNCOV
771
                                }
772
                        case <-done:
1✔
773
                                return
1✔
774
                        }
775
                }
776
        }()
777
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
1✔
778
                ArtifactID:     artifactID,
1✔
779
                ArtifactReader: artifact,
1✔
780
        },
1✔
781
                skipVerify,
1✔
782
        )
1✔
783
        if err != nil {
1✔
784
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
785
                linkStatus = model.LinkStatusAborted
786
        }
787
        errDB := d.db.UpdateUploadIntentStatus(
1✔
788
                ctx, artifactID,
1✔
789
                model.LinkStatusProcessing, linkStatus,
1✔
790
        )
1✔
791
        if errDB != nil {
1✔
792
                l.Warnf("failed to update upload link status: %s", errDB)
793
        }
794
        return err
1✔
795
}
796

797
func (d *Deployments) CompleteUpload(
798
        ctx context.Context,
799
        intentID string,
800
        skipVerify bool,
801
) error {
1✔
802
        l := log.FromContext(ctx)
1✔
803
        idty := identity.FromContext(ctx)
1✔
804
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
805
        if err != nil {
1✔
806
                return err
807
        }
808
        // Create an async context that doesn't cancel when server connection
809
        // closes.
810
        ctxAsync := context.Background()
1✔
811
        ctxAsync = log.WithContext(ctxAsync, l)
1✔
812
        ctxAsync = identity.WithContext(ctxAsync, idty)
1✔
813

1✔
814
        settings, _ := storage.SettingsFromContext(ctx)
1✔
815
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
1✔
816
        var artifactReader io.ReadCloser
1✔
817
        if skipVerify {
2✔
818
                artifactReader, err = d.objectStorage.GetObject(
1✔
819
                        ctxAsync,
1✔
820
                        model.ImagePathFromContext(ctx, intentID),
1✔
821
                )
1✔
822
        } else {
1✔
823
                artifactReader, err = d.objectStorage.GetObject(
824
                        ctxAsync,
825
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
826
                )
827
        }
828
        if err != nil {
1✔
829
                if errors.Is(err, storage.ErrObjectNotFound) {
830
                        return ErrUploadNotFound
831
                }
832
                return err
833
        }
834

835
        err = d.db.UpdateUploadIntentStatus(
1✔
836
                ctx,
1✔
837
                intentID,
1✔
838
                model.LinkStatusPending,
1✔
839
                model.LinkStatusProcessing,
1✔
840
        )
1✔
841
        if err != nil {
1✔
842
                errClose := artifactReader.Close()
843
                if errClose != nil {
844
                        l.Warnf("failed to close artifact reader: %s", errClose)
845
                }
846
                if errors.Is(err, store.ErrNotFound) {
847
                        return ErrUploadNotFound
848
                }
849
                return err
850
        }
851
        go d.processUploadedArtifact( // nolint:errcheck
1✔
852
                ctxAsync, intentID, artifactReader, skipVerify,
1✔
853
        )
1✔
854
        return nil
1✔
855
}
856

857
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
858
        return &model.ArtifactInfo{
1✔
859
                Format:  info.Format,
1✔
860
                Version: uint(info.Version),
1✔
861
        }
1✔
862
}
1✔
863

864
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
865
        var files []model.UpdateFile
1✔
866
        for _, u := range uFiles {
2✔
867
                files = append(files, model.UpdateFile{
1✔
868
                        Name:     u.Name,
1✔
869
                        Size:     u.Size,
1✔
870
                        Date:     &u.Date,
1✔
871
                        Checksum: string(u.Checksum),
1✔
872
                })
1✔
873
        }
1✔
874
        return files, nil
1✔
875
}
876

877
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
1✔
878
        metaArtifact := model.NewArtifactMeta()
1✔
879

1✔
880
        aReader := areader.NewReader(*r)
1✔
881

1✔
882
        // There is no signature verification here.
1✔
883
        // It is just simple check if artifact is signed or not.
1✔
884
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
1✔
UNCOV
885
                metaArtifact.Signed = true
UNCOV
886
                return nil
UNCOV
887
        }
888

889
        var err error
1✔
890
        if skipVerify {
2✔
891
                err = aReader.ReadArtifactHeaders()
1✔
892
                if err != nil {
1✔
893
                        return nil, errors.Wrap(err, "reading artifact error")
894
                }
895
        } else {
1✔
896
                err = aReader.ReadArtifact()
1✔
897
                if err != nil {
2✔
898
                        return nil, errors.Wrap(err, "reading artifact error")
1✔
899
                }
1✔
900
        }
901

902
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
903
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
904

1✔
905
        metaArtifact.Name = aReader.GetArtifactName()
1✔
906
        if metaArtifact.Info.Version == 3 {
2✔
907
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
908
                if err != nil {
1✔
UNCOV
909
                        return nil, errors.Wrap(err,
UNCOV
910
                                "error parsing version 3 artifact")
UNCOV
911
                }
912

913
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
914
                if err != nil {
1✔
UNCOV
915
                        return nil, errors.Wrap(err,
UNCOV
916
                                "error parsing version 3 artifact")
UNCOV
917
                }
918

919
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
920
        }
921

922
        for _, p := range aReader.GetHandlers() {
2✔
923
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
924
                if err != nil {
1✔
925
                        return nil, errors.Wrap(err, "Cannot get update files:")
UNCOV
926
                }
927

928
                uMetadata, err := p.GetUpdateMetaData()
1✔
929
                if err != nil {
1✔
UNCOV
930
                        return nil, errors.Wrap(err, "Cannot get update metadata")
UNCOV
931
                }
932

933
                metaArtifact.Updates = append(
1✔
934
                        metaArtifact.Updates,
1✔
935
                        model.Update{
1✔
936
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
937
                                        Type: p.GetUpdateType(),
1✔
938
                                },
1✔
939
                                Files:    uFiles,
1✔
940
                                MetaData: uMetadata,
1✔
941
                        })
1✔
942
        }
943

944
        return metaArtifact, nil
1✔
945
}
946

947
func getArtifactIDs(artifacts []*model.Image) []string {
1✔
948
        artifactIDs := make([]string, 0, len(artifacts))
1✔
949
        for _, artifact := range artifacts {
2✔
950
                artifactIDs = append(artifactIDs, artifact.Id)
1✔
951
        }
1✔
952
        return artifactIDs
1✔
953
}
954

955
// deployments
956
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
957
        ids := make([]string, len(devices))
958
        for i, d := range devices {
959
                ids[i] = d.ID
960
        }
961

962
        return ids
963
}
964

965
// updateDeploymentConstructor fills devices list with device ids
966
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
967
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
968
        l := log.FromContext(ctx)
969

970
        id := identity.FromContext(ctx)
971
        if id == nil {
UNCOV
972
                l.Error("identity not present in the context")
UNCOV
973
                return nil, ErrModelInternal
UNCOV
974
        }
975
        searchParams := model.SearchParams{
976
                Page:    1,
977
                PerPage: PerPageInventoryDevices,
978
                Filters: []model.FilterPredicate{
979
                        {
980
                                Scope:     InventoryIdentityScope,
981
                                Attribute: InventoryStatusAttributeName,
982
                                Type:      "$eq",
983
                                Value:     InventoryStatusAccepted,
984
                        },
985
                },
986
        }
987
        if len(constructor.Group) > 0 {
988
                searchParams.Filters = append(
989
                        searchParams.Filters,
990
                        model.FilterPredicate{
991
                                Scope:     InventoryGroupScope,
992
                                Attribute: InventoryGroupAttributeName,
993
                                Type:      "$eq",
994
                                Value:     constructor.Group,
995
                        })
996
        }
997

998
        for {
999
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
1000
                if err != nil {
1001
                        l.Errorf("error searching for devices")
1002
                        return nil, ErrModelInternal
1003
                }
1004
                if count < 1 {
1005
                        l.Errorf("no devices found")
1006
                        return nil, ErrNoDevices
1007
                }
1008
                if len(devices) < 1 {
UNCOV
1009
                        break
1010
                }
1011
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
1012
                if len(constructor.Devices) == count {
1013
                        break
1014
                }
1015
                searchParams.Page++
1016
        }
1017

1018
        return constructor, nil
1019
}
1020

1021
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1022
func (d *Deployments) CreateDeviceConfigurationDeployment(
1023
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1024
        deviceID, deploymentID string) (string, error) {
1✔
1025

1✔
1026
        if constructor == nil {
1✔
1027
                return "", ErrModelMissingInput
1028
        }
1029

1030
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
1✔
1031
                constructor,
1✔
1032
                deploymentID,
1✔
1033
        )
1✔
1034
        if err != nil {
1✔
UNCOV
1035
                return "", errors.Wrap(err, "failed to create deployment")
UNCOV
1036
        }
1037

1038
        deployment.DeviceList = []string{deviceID}
1✔
1039
        deployment.MaxDevices = 1
1✔
1040
        deployment.Configuration = []byte(constructor.Configuration)
1✔
1041
        deployment.Type = model.DeploymentTypeConfiguration
1✔
1042

1✔
1043
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
1✔
1044
        if err != nil {
1✔
1045
                return "", err
1046
        }
1047
        deployment.Groups = groups
1✔
1048

1✔
1049
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
2✔
1050
                if strings.Contains(err.Error(), "duplicate key error") {
2✔
1051
                        return "", ErrDuplicateDeployment
1✔
1052
                }
1✔
1053
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
2✔
1054
                        return "", ErrInvalidDeploymentID
1✔
1055
                }
1✔
1056
                return "", errors.Wrap(err, "Storing deployment data")
1057
        }
1058

1059
        return deployment.Id, nil
1✔
1060
}
1061

1062
// CreateDeployment precomputes new deployment and schedules it for devices.
1063
func (d *Deployments) CreateDeployment(ctx context.Context,
1064
        constructor *model.DeploymentConstructor) (string, error) {
1✔
1065

1✔
1066
        var err error
1✔
1067

1✔
1068
        if constructor == nil {
1✔
1069
                return "", ErrModelMissingInput
1070
        }
1071

1072
        if err := constructor.Validate(); err != nil {
1✔
1073
                return "", errors.Wrap(err, "Validating deployment")
1074
        }
1075

1076
        if len(constructor.Group) > 0 || constructor.AllDevices {
1✔
1077
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
1078
                if err != nil {
1079
                        return "", err
1080
                }
1081
        }
1082

1083
        deployment, err := model.NewDeploymentFromConstructor(constructor)
1✔
1084
        if err != nil {
1✔
UNCOV
1085
                return "", errors.Wrap(err, "failed to create deployment")
UNCOV
1086
        }
1087

1088
        // Assign artifacts to the deployment.
1089
        // When new artifact(s) with the artifact name same as the one in the deployment
1090
        // will be uploaded to the backend, it will also become part of this deployment.
1091
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
1✔
1092
        if err != nil {
1✔
UNCOV
1093
                return "", errors.Wrap(err, "Finding artifact with given name")
UNCOV
1094
        }
1095

1096
        if len(artifacts) == 0 {
2✔
1097
                return "", ErrNoArtifact
1✔
1098
        }
1✔
1099

1100
        deployment.Artifacts = getArtifactIDs(artifacts)
1✔
1101
        deployment.DeviceList = constructor.Devices
1✔
1102
        deployment.MaxDevices = len(constructor.Devices)
1✔
1103
        deployment.Type = model.DeploymentTypeSoftware
1✔
1104
        if len(constructor.Group) > 0 {
1✔
1105
                deployment.Groups = []string{constructor.Group}
1106
        }
1107

1108
        // single device deployment case
1109
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
2✔
1110
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
1✔
1111
                if err != nil {
1✔
1112
                        return "", err
UNCOV
1113
                }
1114
                deployment.Groups = groups
1✔
1115
        }
1116

1117
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
1✔
1118
                return "", errors.Wrap(err, "Storing deployment data")
1119
        }
1120

1121
        return deployment.Id, nil
1✔
1122
}
1123

1124
func (d *Deployments) getDeploymentGroups(
1125
        ctx context.Context,
1126
        devices []string,
1127
) ([]string, error) {
1✔
1128
        id := identity.FromContext(ctx)
1✔
1129

1✔
1130
        //only for single device deployment case
1✔
1131
        if len(devices) != 1 {
1✔
1132
                return nil, nil
UNCOV
1133
        }
1134

1135
        if id == nil {
2✔
1136
                id = &identity.Identity{}
1✔
1137
        }
1✔
1138

1139
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
1✔
1140
        if err != nil && err != inventory.ErrDevNotFound {
1✔
1141
                return nil, err
1142
        }
1143
        return groups, nil
1✔
1144
}
1145

1146
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1147
func (d *Deployments) IsDeploymentFinished(
1148
        ctx context.Context,
1149
        deploymentID string,
1150
) (bool, error) {
1✔
1151
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1152
        if err != nil {
1✔
UNCOV
1153
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
UNCOV
1154
        }
1155
        if deployment == nil {
2✔
1156
                return true, nil
1✔
1157
        }
1✔
1158

1159
        return false, nil
1✔
1160
}
1161

1162
// GetDeployment fetches deployment by ID
1163
func (d *Deployments) GetDeployment(ctx context.Context,
1164
        deploymentID string) (*model.Deployment, error) {
1✔
1165

1✔
1166
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1167
        if err != nil {
1✔
UNCOV
1168
                return nil, errors.Wrap(err, "Searching for deployment by ID")
UNCOV
1169
        }
1170

1171
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
UNCOV
1172
                return nil, err
UNCOV
1173
        }
1174

1175
        return deployment, nil
1✔
1176
}
1177

1178
// ImageUsedInActiveDeployment checks if specified image is in use by deployments Image is
1179
// considered to be in use if it's participating in at lest one non success/error deployment.
1180
func (d *Deployments) ImageUsedInActiveDeployment(ctx context.Context,
1181
        imageID string) (bool, error) {
1✔
1182

1✔
1183
        var found bool
1✔
1184

1✔
1185
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
1✔
1186
        if err != nil {
1✔
1187
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1188
        }
1189

1190
        if found {
2✔
1191
                return found, nil
1✔
1192
        }
1✔
1193

1194
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
1✔
1195
                imageID, model.ActiveDeploymentStatuses()...)
1✔
1196
        if err != nil {
1✔
1197
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1198
        }
1199

1200
        return found, nil
1✔
1201
}
1202

1203
// ImageUsedInDeployment checks if specified image is in use by deployments
1204
// Image is considered to be in use if it's participating in any deployment.
UNCOV
1205
func (d *Deployments) ImageUsedInDeployment(ctx context.Context, imageID string) (bool, error) {
1206

1207
        var found bool
UNCOV
1208

UNCOV
1209
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
1210
        if err != nil {
1211
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
UNCOV
1212
        }
1213

UNCOV
1214
        if found {
UNCOV
1215
                return found, nil
UNCOV
1216
        }
1217

UNCOV
1218
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
UNCOV
1219
        if err != nil {
UNCOV
1220
                return false, errors.Wrap(err, "Checking if image is used in deployment")
UNCOV
1221
        }
1222

UNCOV
1223
        return found, nil
1224
}
1225

1226
// Retrieves the model.Deployment and model.DeviceDeployment structures
1227
// for the device. Upon error, nil is returned for both deployment structures.
1228
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1229
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1230

1✔
1231
        // Retrieve device deployment
1✔
1232
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
1✔
1233

1✔
1234
        if err != nil {
1✔
UNCOV
1235
                return nil, nil, errors.Wrap(err,
UNCOV
1236
                        "Searching for oldest active deployment for the device")
1237
        } else if deviceDeployment == nil {
2✔
1238
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1239
        }
1✔
1240

1241
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
1✔
1242
        if err != nil {
1✔
1243
                return nil, nil, errors.Wrap(err, "checking deployment id")
1244
        }
1245
        if deployment == nil {
1✔
1246
                return nil, nil, errors.New("No deployment corresponding to device deployment")
1247
        }
1248

1249
        return deployment, deviceDeployment, nil
1✔
1250
}
1251

1252
// getNewDeploymentForDevice returns deployment object and creates and returns
1253
// new device deployment for the device;
1254
//
1255
// we are interested only in the deployments that are newer than the latest
1256
// deployment applied by the device;
1257
// this way we guarantee that the device will not receive deployment
1258
// that is older than the one installed on the device;
1259
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1260
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1261

1✔
1262
        var lastDeployment *time.Time
1✔
1263
        //get latest device deployment for the device;
1✔
1264
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1265
        if err != nil {
1✔
UNCOV
1266
                return nil, nil, errors.Wrap(err,
UNCOV
1267
                        "Searching for latest active deployment for the device")
1268
        } else if deviceDeployment == nil {
2✔
1269
                lastDeployment = &time.Time{}
1✔
1270
        } else {
2✔
1271
                lastDeployment = deviceDeployment.Created
1✔
1272
        }
1✔
1273

1274
        //get deployments newer then last device deployment
1275
        //iterate over deployments and check if the device is part of the deployment or not
1276
        for skip := 0; true; skip += 100 {
2✔
1277
                deployments, err := d.db.FindNewerActiveDeployments(ctx, lastDeployment, skip, 100)
1✔
1278
                if err != nil {
1✔
UNCOV
1279
                        return nil, nil, errors.Wrap(err,
UNCOV
1280
                                "Failed to search for newer active deployments")
1281
                }
1282
                if len(deployments) == 0 {
2✔
1283
                        return nil, nil, nil
1✔
1284
                }
1✔
1285

1286
                for _, deployment := range deployments {
2✔
1287
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1288
                        if err != nil {
1✔
UNCOV
1289
                                return nil, nil, err
UNCOV
1290
                        }
1291
                        if ok {
2✔
1292
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1293
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1294
                                if err != nil {
1✔
UNCOV
1295
                                        return nil, nil, err
UNCOV
1296
                                }
1297
                                return deployment, deviceDeployment, nil
1✔
1298
                        }
1299
                }
1300
        }
1301

UNCOV
1302
        return nil, nil, nil
1303
}
1304

1305
func (d *Deployments) createDeviceDeploymentWithStatus(
1306
        ctx context.Context, deviceID string,
1307
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1308
) (*model.DeviceDeployment, error) {
1✔
1309
        prevStatus := model.DeviceDeploymentStatusNull
1✔
1310
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
1✔
1311
        if err != nil && err != mongo.ErrStorageNotFound {
1✔
UNCOV
1312
                return nil, err
1313
        } else if deviceDeployment != nil {
1✔
UNCOV
1314
                prevStatus = deviceDeployment.Status
UNCOV
1315
        }
1316

1317
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
1✔
1318
        deviceDeployment.Status = status
1✔
1319
        deviceDeployment.Active = status.Active()
1✔
1320
        deviceDeployment.Created = deployment.Created
1✔
1321

1✔
1322
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
UNCOV
1323
                return nil, err
UNCOV
1324
        }
1325

1326
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
1✔
1327
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
1✔
1328
                return nil, err
UNCOV
1329
        }
1330

1331
        // after inserting new device deployment update deployment stats
1332
        // in the database and locally, and update deployment status
1333
        if err := d.db.UpdateStatsInc(
1✔
1334
                ctx, deployment.Id,
1✔
1335
                prevStatus, status,
1✔
1336
        ); err != nil {
1✔
UNCOV
1337
                return nil, err
UNCOV
1338
        }
1339

1340
        deployment.Stats.Inc(status)
1✔
1341

1✔
1342
        err = d.recalcDeploymentStatus(ctx, deployment)
1✔
1343
        if err != nil {
1✔
UNCOV
1344
                return nil, errors.Wrap(err, "failed to update deployment status")
UNCOV
1345
        }
1346

1347
        if !status.Active() {
1✔
1348
                err := d.reindexDevice(ctx, deviceID)
1349
                if err != nil {
1350
                        l := log.FromContext(ctx)
UNCOV
1351
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
1352
                }
1353
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1354
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
UNCOV
1355
                        l := log.FromContext(ctx)
UNCOV
1356
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
UNCOV
1357
                }
1358
        }
1359

1360
        return deviceDeployment, nil
1✔
1361
}
1362

1363
func (d *Deployments) isDevicePartOfDeployment(
1364
        ctx context.Context,
1365
        deviceID string,
1366
        deployment *model.Deployment,
1367
) (bool, error) {
1✔
1368
        for _, id := range deployment.DeviceList {
2✔
1369
                if id == deviceID {
2✔
1370
                        return true, nil
1✔
1371
                }
1✔
1372
        }
1373
        return false, nil
1✔
1374
}
1375

1376
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1377
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1378
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
1✔
1379

1✔
1380
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
1✔
1381
        if err != nil {
1✔
1382
                return nil, ErrModelInternal
1383
        } else if deployment == nil {
2✔
1384
                return nil, nil
1✔
1385
        }
1✔
1386

1387
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
1✔
1388
        if err != nil {
2✔
1389
                return nil, err
1✔
1390
        }
1✔
1391
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
1✔
1392
}
1393

1394
func (d *Deployments) getDeploymentInstructions(
1395
        ctx context.Context,
1396
        deployment *model.Deployment,
1397
        deviceDeployment *model.DeviceDeployment,
1398
        request *model.DeploymentNextRequest,
1399
) (*model.DeploymentInstructions, error) {
1✔
1400

1✔
1401
        var newArtifactAssigned bool
1✔
1402

1✔
1403
        l := log.FromContext(ctx)
1✔
1404

1✔
1405
        if deployment.Type == model.DeploymentTypeConfiguration {
2✔
1406
                // There's nothing more we need to do, the link must be filled
1✔
1407
                // in by the API layer.
1✔
1408
                return &model.DeploymentInstructions{
1✔
1409
                        ID: deployment.Id,
1✔
1410
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1411
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1412
                                // use deployment ID togheter with device ID as artifact ID
1✔
1413
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1414
                                ArtifactName:          deployment.ArtifactName,
1✔
1415
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1416
                        },
1✔
1417
                        Type: model.DeploymentTypeConfiguration,
1✔
1418
                }, nil
1✔
1419
        }
1✔
1420

1421
        // assing artifact to the device deployment
1422
        // only if it was not assgined previously
1423
        if deviceDeployment.Image == nil {
2✔
1424
                if err := d.assignArtifact(
1✔
1425
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
1✔
UNCOV
1426
                        return nil, err
UNCOV
1427
                }
1428
                newArtifactAssigned = true
1✔
1429
        }
1430

1431
        if deviceDeployment.Image == nil {
1✔
UNCOV
1432
                // No artifact - return empty response
UNCOV
1433
                return nil, nil
UNCOV
1434
        }
1435

1436
        // if the deployment is not forcing the installation, and
1437
        // if artifact was recognized as already installed, and this is
1438
        // a new device deployment - indicated by device deployment status "pending",
1439
        // handle already installed artifact case
1440
        if !deployment.ForceInstallation &&
1✔
1441
                d.isAlreadyInstalled(request, deviceDeployment) &&
1✔
1442
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
2✔
1443
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
1✔
1444
        }
1✔
1445

1446
        // if new artifact has been assigned to device deployment
1447
        // add artifact size to deployment total size,
1448
        // before returning deployment instruction to the device
1449
        if newArtifactAssigned {
2✔
1450
                if err := d.db.IncrementDeploymentTotalSize(
1✔
1451
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
UNCOV
1452
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
UNCOV
1453
                }
1454
        }
1455

1456
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1457
        if err != nil {
1✔
UNCOV
1458
                return nil, err
UNCOV
1459
        }
1460

1461
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1462
        link, err := d.objectStorage.GetRequest(
1✔
1463
                ctx,
1✔
1464
                imagePath,
1✔
1465
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1466
                DefaultUpdateDownloadLinkExpire,
1✔
1467
        )
1✔
1468
        if err != nil {
1✔
UNCOV
1469
                return nil, errors.Wrap(err, "Generating download link for the device")
1470
        }
1471

1472
        instructions := &model.DeploymentInstructions{
1✔
1473
                ID: deviceDeployment.DeploymentId,
1✔
1474
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1475
                        ID: deviceDeployment.Image.Id,
1✔
1476
                        ArtifactName: deviceDeployment.Image.
1✔
1477
                                ArtifactMeta.Name,
1✔
1478
                        Source: *link,
1✔
1479
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1480
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1481
                },
1✔
1482
        }
1✔
1483

1✔
1484
        return instructions, nil
1✔
1485
}
1486

1487
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1488
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
1✔
1489
        if deviceDeployment.Request != nil {
2✔
1490
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1491
                        // the device reported different device type and/or artifact name
1✔
1492
                        // during the update process, which should never happen;
1✔
1493
                        // mark deployment for this device as failed to force client to rollback
1✔
1494
                        l := log.FromContext(ctx)
1✔
1495
                        l.Errorf(
1✔
1496
                                "Device with id %s reported new data: %s during update process;"+
1✔
1497
                                        "old data: %s",
1✔
1498
                                deviceID, request, deviceDeployment.Request)
1✔
1499

1✔
1500
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1501
                                model.DeviceDeploymentState{
1✔
1502
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1503
                                }); err != nil {
1✔
UNCOV
1504
                                return errors.Wrap(err, "Failed to update deployment status")
UNCOV
1505
                        }
1506
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1507
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
1508
                        }
1509
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1510
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
UNCOV
1511
                                l := log.FromContext(ctx)
UNCOV
1512
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
UNCOV
1513
                        }
1514
                        return ErrConflictingRequestData
1✔
1515
                }
1516
        } else {
1✔
1517
                // save the request
1✔
1518
                if err := d.db.SaveDeviceDeploymentRequest(
1✔
1519
                        ctx, deviceDeployment.Id, request); err != nil {
1✔
UNCOV
1520
                        return err
UNCOV
1521
                }
1522
        }
1523
        return nil
1✔
1524
}
1525

1526
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1527
// ID `deviceID`. Returns nil if update was successful.
1528
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1529
        deviceID string, ddState model.DeviceDeploymentState) error {
1✔
1530

1✔
1531
        l := log.FromContext(ctx)
1✔
1532

1✔
1533
        l.Infof("New status: %s for device %s deployment: %v", ddState.Status, deviceID, deploymentID)
1✔
1534

1✔
1535
        var finishTime *time.Time = nil
1✔
1536
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
2✔
1537
                now := time.Now()
1✔
1538
                finishTime = &now
1✔
1539
        }
1✔
1540

1541
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
1✔
1542
        if err == mongo.ErrStorageNotFound {
1✔
1543
                return ErrStorageNotFound
1544
        } else if err != nil {
1✔
1545
                return err
1546
        }
1547

1548
        currentStatus := dd.Status
1✔
1549

1✔
1550
        if currentStatus == model.DeviceDeploymentStatusAborted {
1✔
1551
                return ErrDeploymentAborted
UNCOV
1552
        }
1553

1554
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
1✔
UNCOV
1555
                return ErrDeviceDecommissioned
UNCOV
1556
        }
1557

1558
        // nothing to do
1559
        if ddState.Status == currentStatus {
1✔
UNCOV
1560
                return nil
UNCOV
1561
        }
1562

1563
        // update finish time
1564
        ddState.FinishTime = finishTime
1✔
1565

1✔
1566
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
1✔
1567
                deviceID, deploymentID, ddState)
1✔
1568
        if err != nil {
1✔
UNCOV
1569
                return err
UNCOV
1570
        }
1571

1572
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
1✔
UNCOV
1573
                return err
UNCOV
1574
        }
1575

1576
        // fetch deployment stats and update deployment status
1577
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1578
        if err != nil {
1✔
UNCOV
1579
                return errors.Wrap(err, "failed when searching for deployment")
UNCOV
1580
        }
1581

1582
        err = d.recalcDeploymentStatus(ctx, deployment)
1✔
1583
        if err != nil {
1✔
1584
                return errors.Wrap(err, "failed to update deployment status")
UNCOV
1585
        }
1586

1587
        if !ddState.Status.Active() {
2✔
1588
                l := log.FromContext(ctx)
1✔
1589
                ldd := model.DeviceDeployment{
1✔
1590
                        DeviceId:     dd.DeviceId,
1✔
1591
                        DeploymentId: dd.DeploymentId,
1✔
1592
                        Id:           dd.Id,
1✔
1593
                        Status:       ddState.Status,
1✔
1594
                }
1✔
1595
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
1✔
UNCOV
1596
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
UNCOV
1597
                }
1598
                if err := d.reindexDevice(ctx, deviceID); err != nil {
1✔
1599
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
UNCOV
1600
                }
1601
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
1✔
UNCOV
1602
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
UNCOV
1603
                }
1604
        }
1605

1606
        return nil
1✔
1607
}
1608

1609
// recalcDeploymentStatus inspects the deployment stats and
1610
// recalculates and updates its status
1611
// it should be used whenever deployment stats are touched
1612
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
1✔
1613
        status := dep.GetStatus()
1✔
1614

1✔
1615
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
1✔
UNCOV
1616
                return err
1617
        }
1618

1619
        return nil
1✔
1620
}
1621

1622
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1623
        deploymentID string) (model.Stats, error) {
1✔
1624

1✔
1625
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1626

1✔
1627
        if err != nil {
1✔
UNCOV
1628
                return nil, errors.Wrap(err, "checking deployment id")
UNCOV
1629
        }
1630

1631
        if deployment == nil {
1✔
UNCOV
1632
                return nil, nil
UNCOV
1633
        }
1634

1635
        return deployment.Stats, nil
1✔
1636
}
1637
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1638
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
UNCOV
1639

1640
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
1641

UNCOV
1642
        if err != nil {
UNCOV
1643
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
UNCOV
1644
        }
1645

UNCOV
1646
        if deploymentStats == nil {
UNCOV
1647
                return nil, ErrModelDeploymentNotFound
UNCOV
1648
        }
1649

UNCOV
1650
        return deploymentStats, nil
1651
}
1652

1653
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1654
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1655
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1656

1✔
1657
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1658
        if err != nil {
1✔
UNCOV
1659
                return nil, ErrModelInternal
UNCOV
1660
        }
1661

1662
        if deployment == nil {
1✔
UNCOV
1663
                return nil, ErrModelDeploymentNotFound
UNCOV
1664
        }
1665

1666
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1667
        if err != nil {
1✔
UNCOV
1668
                return nil, ErrModelInternal
UNCOV
1669
        }
1670

1671
        return statuses, nil
1✔
1672
}
1673

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

1✔
1677
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1678
        if err != nil {
1✔
1679
                return nil, -1, ErrModelInternal
1680
        }
1681

1682
        if deployment == nil {
1✔
UNCOV
1683
                return nil, -1, ErrModelDeploymentNotFound
1684
        }
1685

1686
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1687
        if err != nil {
1✔
1688
                return nil, -1, ErrModelInternal
UNCOV
1689
        }
1690

1691
        return statuses, totalCount, nil
1✔
1692
}
1693

1694
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1695
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
1696
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
1697
        if err != nil {
1698
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1699
        }
1700

1701
        deploymentIDs := make([]string, len(deviceDeployments))
1702
        for i, deviceDeployment := range deviceDeployments {
1703
                deploymentIDs[i] = deviceDeployment.DeploymentId
1704
        }
1705

1706
        deployments, _, err := d.db.Find(ctx, model.Query{
1707
                IDs:          deploymentIDs,
1708
                Limit:        len(deviceDeployments),
1709
                DisableCount: true,
1710
        })
1711
        if err != nil {
1712
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1713
        }
1714

1715
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
1716
        for _, deployment := range deployments {
1717
                deploymentsMap[deployment.Id] = deployment
1718
        }
1719

1720
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
1721
        for i, deviceDeployment := range deviceDeployments {
1722
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
1723
                        res = append(res, model.DeviceDeploymentListItem{
1724
                                Id:         deviceDeployment.Id,
1725
                                Deployment: deployment,
1726
                                Device:     &deviceDeployments[i],
1727
                        })
1728
                } else {
1729
                        res = append(res, model.DeviceDeploymentListItem{
1730
                                Id:     deviceDeployment.Id,
1731
                                Device: &deviceDeployments[i],
1732
                        })
1733
                }
1734
        }
1735

1736
        return res, totalCount, nil
1737
}
1738

1739
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1740
        ctx context.Context,
1741
        deployment *model.Deployment,
1742
) error {
1✔
1743
        if deployment.DeviceCount == nil {
1✔
UNCOV
1744
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
UNCOV
1745
                if err != nil {
UNCOV
1746
                        return errors.Wrap(err, "counting device deployments")
UNCOV
1747
                }
UNCOV
1748
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
UNCOV
1749
                if err != nil {
UNCOV
1750
                        return errors.Wrap(err, "setting the device count for the deployment")
UNCOV
1751
                }
UNCOV
1752
                deployment.DeviceCount = &deviceCount
1753
        }
1754

1755
        return nil
1✔
1756
}
1757

1758
func (d *Deployments) LookupDeployment(ctx context.Context,
1759
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1760
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1761

1✔
1762
        if err != nil {
1✔
UNCOV
1763
                return nil, 0, errors.Wrap(err, "searching for deployments")
UNCOV
1764
        }
1765

1766
        if list == nil {
2✔
1767
                return make([]*model.Deployment, 0), 0, nil
1✔
1768
        }
1✔
1769

UNCOV
1770
        for _, deployment := range list {
UNCOV
1771
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
UNCOV
1772
                        return nil, 0, err
UNCOV
1773
                }
1774
        }
1775

UNCOV
1776
        return list, totalCount, nil
1777
}
1778

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

1✔
1784
        // repack to temporary deployment log and validate
1✔
1785
        dlog := model.DeploymentLog{
1✔
1786
                DeviceID:     deviceID,
1✔
1787
                DeploymentID: deploymentID,
1✔
1788
                Messages:     logs,
1✔
1789
        }
1✔
1790
        if err := dlog.Validate(); err != nil {
1✔
UNCOV
1791
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
UNCOV
1792
        }
1793

1794
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
UNCOV
1795
                if err != nil {
UNCOV
1796
                        return err
UNCOV
1797
                } else {
UNCOV
1798
                        return ErrModelDeploymentNotFound
UNCOV
1799
                }
1800
        }
1801

1802
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
UNCOV
1803
                return err
UNCOV
1804
        }
1805

1806
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1807
                deviceID, deploymentID, true)
1✔
1808
}
1809

1810
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1811
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1812

1✔
1813
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1814
                deviceID, deploymentID)
1✔
1815
}
1✔
1816

1817
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1818
        deploymentID string, deviceID string) (bool, error) {
1✔
1819
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1820
}
1✔
1821

1822
// AbortDeployment aborts deployment for devices and updates deployment stats
1823
func (d *Deployments) AbortDeployment(ctx context.Context, deploymentID string) error {
1✔
1824

1✔
1825
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
1✔
1826
                return err
1827
        }
1828

1829
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
1✔
1830
                ctx, deploymentID)
1✔
1831
        if err != nil {
1✔
1832
                return err
1833
        }
1834

1835
        // update statistics
1836
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
1✔
1837
                return errors.Wrap(err, "failed to update deployment stats")
1838
        }
1839

1840
        // when aborting the deployment we need to set status directly instead of
1841
        // using recalcDeploymentStatus method;
1842
        // it is possible that the deployment does not have any device deployments yet;
1843
        // in that case, all statistics are 0 and calculating status based on statistics
1844
        // will not work - the calculated status will be "pending"
1845
        if err := d.db.SetDeploymentStatus(ctx,
1✔
1846
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
1✔
UNCOV
1847
                return errors.Wrap(err, "failed to update deployment status")
UNCOV
1848
        }
1849

1850
        return nil
1✔
1851
}
1852

1853
func (d *Deployments) updateDeviceDeploymentsStatus(
1854
        ctx context.Context,
1855
        deviceId string,
1856
        status model.DeviceDeploymentStatus,
1857
) error {
1858
        var latestDeployment *time.Time
1859
        // Retrieve active device deployment for the device
1860
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
1861
        if err != nil {
1862
                return errors.Wrap(err, "Searching for active deployment for the device")
1863
        } else if deviceDeployment != nil {
1864
                now := time.Now()
1865
                ddStatus := model.DeviceDeploymentState{
1866
                        Status:     status,
1867
                        FinishTime: &now,
1868
                }
1869
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
1870
                        deviceId, ddStatus); err != nil {
UNCOV
1871
                        return errors.Wrap(err, "updating device deployment status")
UNCOV
1872
                }
1873
                latestDeployment = deviceDeployment.Created
1874
        } else {
1875
                // get latest device deployment for the device
1876
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
1877
                if err != nil {
UNCOV
1878
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
1879
                } else if deviceDeployment == nil {
1880
                        latestDeployment = &time.Time{}
1881
                } else {
1882
                        latestDeployment = deviceDeployment.Created
1883
                }
1884
        }
1885

1886
        // get deployments newer then last device deployment
1887
        // iterate over deployments and check if the device is part of the deployment or not
1888
        // if the device is part of the deployment create new, decommisioned device deployment
1889
        for skip := 0; true; skip += 100 {
1890
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
1891
                if err != nil {
UNCOV
1892
                        return errors.Wrap(err, "Failed to search for newer active deployments")
UNCOV
1893
                }
1894
                if len(deployments) == 0 {
1895
                        break
1896
                }
1897
                for _, deployment := range deployments {
1898
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
1899
                        if err != nil {
UNCOV
1900
                                return err
UNCOV
1901
                        }
1902
                        if ok {
1903
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1904
                                        deviceId, deployment, status)
1905
                                if err != nil {
UNCOV
1906
                                        return err
UNCOV
1907
                                }
1908
                                if !status.Active() {
1909
                                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1910
                                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
UNCOV
1911
                                                l := log.FromContext(ctx)
UNCOV
1912
                                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
UNCOV
1913
                                        }
1914
                                }
1915
                        }
1916
                }
1917
        }
1918

1919
        if err := d.reindexDevice(ctx, deviceId); err != nil {
UNCOV
1920
                l := log.FromContext(ctx)
UNCOV
1921
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
UNCOV
1922
        }
1923

1924
        return nil
1925
}
1926

1927
// DecommissionDevice updates the status of all the pending and active deployments for a device
1928
// to decommissioned
1929
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
1930
        return d.updateDeviceDeploymentsStatus(
1931
                ctx,
1932
                deviceId,
1933
                model.DeviceDeploymentStatusDecommissioned,
1934
        )
1935
}
1936

1937
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1938
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
1939
        return d.updateDeviceDeploymentsStatus(
1940
                ctx,
1941
                deviceId,
1942
                model.DeviceDeploymentStatusAborted,
1943
        )
1944
}
1945

1946
// DeleteDeviceDeploymentsHistory deletes the device deployments history
1947
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
1948
        // get device deployments which will be marked as deleted
1949
        f := false
1950
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
1951
        if err != nil {
UNCOV
1952
                return err
UNCOV
1953
        }
1954

1955
        // no device deployments to update
1956
        if len(dd) <= 0 {
UNCOV
1957
                return nil
1958
        }
1959

1960
        // mark device deployments as deleted
1961
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
1962
                return err
1963
        }
1964

1965
        // trigger reindexing of updated device deployments
1966
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1967
        for i, d := range dd {
1968
                deviceDeployments[i].ID = d.Id
1969
                deviceDeployments[i].DeviceID = d.DeviceId
1970
                deviceDeployments[i].DeploymentID = d.DeploymentId
1971
        }
1972
        return d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1973
}
1974

1975
// Storage settings
1976
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
1✔
1977
        settings, err := d.db.GetStorageSettings(ctx)
1✔
1978
        if err != nil {
1✔
1979
                return nil, errors.Wrap(err, "Searching for settings failed")
1980
        }
1981

1982
        return settings, nil
1✔
1983
}
1984

1985
func (d *Deployments) SetStorageSettings(
1986
        ctx context.Context,
1987
        storageSettings *model.StorageSettings,
1988
) error {
1✔
1989
        if storageSettings != nil {
2✔
1990
                ctx = storage.SettingsWithContext(ctx, storageSettings)
1✔
1991
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
1✔
UNCOV
1992
                        return errors.WithMessage(err,
UNCOV
1993
                                "the provided storage settings failed the health check",
UNCOV
1994
                        )
1995
                }
1996
        }
1997
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
1✔
1998
                return errors.Wrap(err, "Failed to save settings")
1999
        }
2000

2001
        return nil
1✔
2002
}
2003

2004
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
1✔
2005
        d.reportingClient = c
1✔
2006
        return d
1✔
2007
}
1✔
2008

2009
func (d *Deployments) haveReporting() bool {
2010
        return d.reportingClient != nil
2011
}
2012

2013
func (d *Deployments) search(
2014
        ctx context.Context,
2015
        tid string,
2016
        parms model.SearchParams,
2017
) ([]model.InvDevice, int, error) {
2018
        if d.haveReporting() {
2019
                return d.reportingClient.Search(ctx, tid, parms)
2020
        } else {
2021
                return d.inventoryClient.Search(ctx, tid, parms)
2022
        }
2023
}
2024

2025
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2026
        ctx context.Context,
2027
        artifactName string,
2028
) error {
1✔
2029
        // first check if there are pending deployments with given artifact name
1✔
2030
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
1✔
2031
        if err != nil {
1✔
2032
                return errors.Wrap(err, "looking for deployments with given artifact name")
2033
        }
2034
        if !exists {
2✔
2035
                return nil
1✔
2036
        }
1✔
2037

2038
        // Assign artifacts to the deployments with given artifact name
2039
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
2040
        if err != nil {
UNCOV
2041
                return errors.Wrap(err, "Finding artifact with given name")
UNCOV
2042
        }
2043

2044
        if len(artifacts) == 0 {
UNCOV
2045
                return ErrNoArtifact
UNCOV
2046
        }
2047
        artifactIDs := getArtifactIDs(artifacts)
2048
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
2049
}
2050

2051
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
1✔
2052
        if d.reportingClient != nil {
2✔
2053
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
1✔
2054
        }
1✔
2055
        return nil
2056
}
2057

2058
func (d *Deployments) reindexDeployment(ctx context.Context,
2059
        deviceID, deploymentID, ID string) error {
1✔
2060
        if d.reportingClient != nil {
2✔
2061
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
1✔
2062
        }
1✔
2063
        return nil
2064
}
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