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

mendersoftware / deployments / 972758132

18 Aug 2023 09:24PM UTC coverage: 79.725% (+0.1%) from 79.613%
972758132

Pull #901

gitlab-ci

merlin-northern
test: mocks and units

Ticket: MEN-6623
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #901: feat: save and get the update types

85 of 93 new or added lines in 5 files covered. (91.4%)

384 existing lines in 3 files now uncovered.

7821 of 9810 relevant lines covered (79.72%)

33.99 hits per line

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

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

15
package app
16

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

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

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

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

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

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

61
        fileSuffixTmp = ".tmp"
62

63
        inprogressIdleTime = time.Hour
64
)
65

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

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

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

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

105
//deployments
106

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

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

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

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

190
        // releases
191
        ReplaceReleaseTags(ctx context.Context, releaseName string, tags model.Tags) error
192
        UpdateRelease(ctx context.Context, releaseName string, release model.ReleasePatch) error
193
        ListReleaseTags(ctx context.Context) (model.Tags, error)
194
        GetReleasesUpdateTypes(ctx context.Context) ([]string, error)
195
}
196

197
type Deployments struct {
198
        db              store.DataStore
199
        objectStorage   storage.ObjectStorage
200
        workflowsClient workflows.Client
201
        inventoryClient inventory.Client
202
        reportingClient reporting.Client
203
}
204

205
// Compile-time check
206
var _ App = &Deployments{}
207

208
func NewDeployments(
209
        storage store.DataStore,
210
        objectStorage storage.ObjectStorage,
211
) *Deployments {
74✔
212
        return &Deployments{
74✔
213
                db:              storage,
74✔
214
                objectStorage:   objectStorage,
74✔
215
                workflowsClient: workflows.NewClient(),
74✔
216
                inventoryClient: inventory.NewClient(),
74✔
217
        }
74✔
218
}
74✔
219

220
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
4✔
221
        d.workflowsClient = workflowsClient
4✔
222
}
4✔
223

224
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
8✔
225
        d.inventoryClient = inventoryClient
8✔
226
}
8✔
227

228
func (d *Deployments) HealthCheck(ctx context.Context) error {
6✔
229
        err := d.db.Ping(ctx)
6✔
230
        if err != nil {
7✔
231
                return errors.Wrap(err, "error reaching MongoDB")
1✔
232
        }
1✔
233
        err = d.objectStorage.HealthCheck(ctx)
5✔
234
        if err != nil {
6✔
235
                return errors.Wrap(
1✔
236
                        err,
1✔
237
                        "error reaching artifact storage service",
1✔
238
                )
1✔
239
        }
1✔
240

241
        err = d.workflowsClient.CheckHealth(ctx)
4✔
242
        if err != nil {
5✔
243
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
244
        }
1✔
245

246
        err = d.inventoryClient.CheckHealth(ctx)
3✔
247
        if err != nil {
4✔
248
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
249
        }
1✔
250

251
        if d.reportingClient != nil {
4✔
252
                err = d.reportingClient.CheckHealth(ctx)
2✔
253
                if err != nil {
3✔
254
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
255
                }
1✔
256
        }
257
        return nil
1✔
258
}
259

260
func (d *Deployments) contextWithStorageSettings(
261
        ctx context.Context,
262
) (context.Context, error) {
26✔
263
        var err error
26✔
264
        settings, ok := storage.SettingsFromContext(ctx)
26✔
265
        if !ok {
48✔
266
                settings, err = d.db.GetStorageSettings(ctx)
22✔
267
        }
22✔
268
        if err != nil {
28✔
269
                return nil, err
2✔
270
        } else if settings != nil {
26✔
271
                err = settings.Validate()
×
272
                if err != nil {
×
273
                        return nil, err
×
274
                }
×
275
        }
276
        return storage.SettingsWithContext(ctx, settings), nil
24✔
277
}
278

279
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
3✔
280
        limit, err := d.db.GetLimit(ctx, name)
3✔
281
        if err == mongo.ErrLimitNotFound {
4✔
282
                return &model.Limit{
1✔
283
                        Name:  name,
1✔
284
                        Value: 0,
1✔
285
                }, nil
1✔
286

1✔
287
        } else if err != nil {
4✔
288
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
289
        }
1✔
290
        return limit, nil
1✔
291
}
292

293
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
3✔
294
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
4✔
295
                return errors.Wrap(err, "failed to provision tenant")
1✔
296
        }
1✔
297

298
        return nil
2✔
299
}
300

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

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

5✔
317
        l := log.FromContext(ctx)
5✔
318
        ctx, err := d.contextWithStorageSettings(ctx)
5✔
319
        if err != nil {
5✔
320
                return "", err
×
321
        }
×
322

323
        // create pipe
324
        pR, pW := io.Pipe()
5✔
325

5✔
326
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
5✔
327

5✔
328
        tee := io.TeeReader(artifactReader, pW)
5✔
329

5✔
330
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
5✔
331
        if err != nil {
6✔
332
                uid, _ = uuid.NewRandom()
1✔
333
        }
1✔
334
        artifactID := uid.String()
5✔
335

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

360
        // parse artifact
361
        // artifact library reads all the data from the given reader
362
        metaArtifactConstructor, err := getMetaFromArchive(&tee, skipVerify)
5✔
363
        if err != nil {
10✔
364
                _ = pW.CloseWithError(err)
5✔
365
                <-ch
5✔
366
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
5✔
367
        }
5✔
368
        // validate artifact metadata
369
        if err = metaArtifactConstructor.Validate(); err != nil {
1✔
370
                return artifactID, ErrModelInvalidMetadata
×
371
        }
×
372

373
        if !skipVerify {
2✔
374
                // read the rest of the data,
1✔
375
                // just in case the artifact library did not read all the data from the reader
1✔
376
                _, err = io.Copy(io.Discard, tee)
1✔
377
                if err != nil {
1✔
378
                        // CloseWithError will cause the reading end to abort upload.
×
379
                        _ = pW.CloseWithError(err)
×
380
                        <-ch
×
381
                        return artifactID, err
×
382
                }
×
383
        }
384

385
        // close the pipe
386
        pW.Close()
1✔
387

1✔
388
        // collect output from the goroutine
1✔
389
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
390
                return artifactID, uploadResponseErr
×
391
        }
×
392

393
        image := model.NewImage(
1✔
394
                artifactID,
1✔
395
                multipartUploadMsg.MetaConstructor,
1✔
396
                metaArtifactConstructor,
1✔
397
                artifactReader.Count(),
1✔
398
        )
1✔
399

1✔
400
        // here
1✔
401
        // image.ArtifactMeta.Updates[0].TypeInfo
1✔
402
        // save image structure in the system
1✔
403
        if err = d.db.InsertImage(ctx, image); err != nil {
1✔
404
                // Try to remove the storage from s3.
×
405
                if errDelete := d.objectStorage.DeleteObject(
×
406
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
407
                ); errDelete != nil {
×
408
                        l.Errorf(
×
409
                                "failed to clean up artifact storage after failure: %s",
×
410
                                errDelete,
×
411
                        )
×
412
                }
×
413
                if idxErr, ok := err.(*model.ConflictError); ok {
×
414
                        return artifactID, idxErr
×
UNCOV
415
                }
×
416
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
417
        }
418
        i := 0
1✔
419
        var updateTypes []string
1✔
420
        if image.ArtifactMeta != nil {
2✔
421
                updateTypes = make([]string, len(image.ArtifactMeta.Updates))
1✔
422
                for _, t := range image.ArtifactMeta.Updates {
2✔
423
                        if t.TypeInfo.Type == nil {
2✔
424
                                continue
1✔
425
                        }
426
                        updateTypes[i] = *t.TypeInfo.Type
1✔
427
                        i++
1✔
428
                }
429
        }
430
        if i > 0 {
2✔
431
                err = d.db.SaveUpdateTypes(ctx, updateTypes[:i])
1✔
432
                if err != nil {
1✔
NEW
433
                        l.Errorf(
×
NEW
UNCOV
434
                                "error while saving the update types for the artifact: %s",
×
NEW
UNCOV
435
                                err.Error(),
×
NEW
436
                        )
×
NEW
437
                }
×
438
        }
439
        // update release
440
        if err := d.updateRelease(ctx, image, nil); err != nil {
1✔
441
                return "", err
×
UNCOV
442
        }
×
443

444
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
UNCOV
445
                return "", errors.Wrap(err, "fail to update deployments")
×
UNCOV
446
        }
×
447

448
        return artifactID, nil
1✔
449
}
450

451
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
452
// creates image structure in the system, and starts the workflow to generate the
453
// artifact from them.
454
// Returns image ID and nil on success.
455
func (d *Deployments) GenerateImage(ctx context.Context,
456
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
11✔
457

11✔
458
        if multipartGenerateImageMsg == nil {
12✔
459
                return "", ErrModelMultipartUploadMsgMalformed
1✔
460
        }
1✔
461

462
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
10✔
463
        if err != nil {
15✔
464
                return "", err
5✔
465
        }
5✔
466
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
6✔
467
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
468
        }
1✔
469
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
5✔
470
        if err != nil {
7✔
471
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
3✔
472
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
473
                }
1✔
474
                return "", err
1✔
475
        }
476

477
        return multipartGenerateImageMsg.ArtifactID, err
3✔
478
}
479

480
func (d *Deployments) GenerateConfigurationImage(
481
        ctx context.Context,
482
        deviceType string,
483
        deploymentID string,
484
) (io.Reader, error) {
5✔
485
        var buf bytes.Buffer
5✔
486
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
487
        if err != nil {
6✔
488
                return nil, err
1✔
489
        } else if dpl == nil {
6✔
490
                return nil, ErrModelDeploymentNotFound
1✔
491
        }
1✔
492
        var metaData map[string]interface{}
3✔
493
        err = json.Unmarshal(dpl.Configuration, &metaData)
3✔
494
        if err != nil {
4✔
495
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
1✔
496
        }
1✔
497

498
        artieWriter := awriter.NewWriter(&buf, artifact.NewCompressorNone())
2✔
499
        module := handlers.NewModuleImage(ArtifactConfigureType)
2✔
500
        err = artieWriter.WriteArtifact(&awriter.WriteArtifactArgs{
2✔
501
                Format:  "mender",
2✔
502
                Version: 3,
2✔
503
                Devices: []string{deviceType},
2✔
504
                Name:    dpl.ArtifactName,
2✔
505
                Updates: &awriter.Updates{Updates: []handlers.Composer{module}},
2✔
506
                Depends: &artifact.ArtifactDepends{
2✔
507
                        CompatibleDevices: []string{deviceType},
2✔
508
                },
2✔
509
                Provides: &artifact.ArtifactProvides{
2✔
510
                        ArtifactName: dpl.ArtifactName,
2✔
511
                },
2✔
512
                MetaData: metaData,
2✔
513
                TypeInfoV3: &artifact.TypeInfoV3{
2✔
514
                        Type: &ArtifactConfigureType,
2✔
515
                        ArtifactProvides: artifact.TypeInfoProvides{
2✔
516
                                ArtifactConfigureProvides: dpl.ArtifactName,
2✔
517
                        },
2✔
518
                        ArtifactDepends:        artifact.TypeInfoDepends{},
2✔
519
                        ClearsArtifactProvides: []string{ArtifactConfigureProvidesCleared},
2✔
520
                },
2✔
521
        })
2✔
522

2✔
523
        return &buf, err
2✔
524
}
525

526
// handleRawFile parses raw data, uploads it to the file storage,
527
// and starts the workflow to generate the artifact.
528
// Returns the object path to the file and nil on success.
529
func (d *Deployments) handleRawFile(ctx context.Context,
530
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
10✔
531
        l := log.FromContext(ctx)
10✔
532
        uid, _ := uuid.NewRandom()
10✔
533
        artifactID := uid.String()
10✔
534
        multipartMsg.ArtifactID = artifactID
10✔
535
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
10✔
536

10✔
537
        // check if artifact is unique
10✔
538
        // artifact is considered to be unique if there is no artifact with the same name
10✔
539
        // and supporting the same platform in the system
10✔
540
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
10✔
541
                multipartMsg.Name,
10✔
542
                multipartMsg.DeviceTypesCompatible,
10✔
543
        )
10✔
544
        if err != nil {
11✔
545
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
1✔
546
        }
1✔
547
        if !isArtifactUnique {
10✔
548
                return "", ErrModelArtifactNotUnique
1✔
549
        }
1✔
550

551
        ctx, err = d.contextWithStorageSettings(ctx)
8✔
552
        if err != nil {
8✔
UNCOV
553
                return "", err
×
UNCOV
554
        }
×
555
        err = d.objectStorage.PutObject(
8✔
556
                ctx, filePath, multipartMsg.FileReader,
8✔
557
        )
8✔
558
        if err != nil {
9✔
559
                return "", err
1✔
560
        }
1✔
561
        defer func() {
14✔
562
                if err != nil {
9✔
563
                        e := d.objectStorage.DeleteObject(ctx, filePath)
2✔
564
                        if e != nil {
4✔
565
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
2✔
566
                                        filePath, e)
2✔
567
                        }
2✔
568
                }
569
        }()
570

571
        link, err := d.objectStorage.GetRequest(
7✔
572
                ctx,
7✔
573
                filePath,
7✔
574
                path.Base(filePath),
7✔
575
                DefaultImageGenerationLinkExpire,
7✔
576
        )
7✔
577
        if err != nil {
8✔
578
                return "", err
1✔
579
        }
1✔
580
        multipartMsg.GetArtifactURI = link.Uri
6✔
581

6✔
582
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
6✔
583
        if err != nil {
7✔
584
                return "", err
1✔
585
        }
1✔
586
        multipartMsg.DeleteArtifactURI = link.Uri
5✔
587

5✔
588
        return artifactID, nil
5✔
589
}
590

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

1✔
595
        image, err := d.db.FindImageByID(ctx, id)
1✔
596
        if err != nil {
1✔
UNCOV
597
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
UNCOV
598
        }
×
599

600
        if image == nil {
2✔
601
                return nil, nil
1✔
602
        }
1✔
603

604
        return image, nil
1✔
605
}
606

607
// DeleteImage removes metadata and image file
608
// Noop for not existing images
609
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
610
// file is needed
611
// In case of already finished updates only image file is not needed, metadata is attached directly
612
// to device deployment therefore we still have some information about image that have been used
613
// (but not the file)
614
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
1✔
615
        found, err := d.GetImage(ctx, imageID)
1✔
616

1✔
617
        if err != nil {
1✔
618
                return errors.Wrap(err, "Getting image metadata")
×
UNCOV
619
        }
×
620

621
        if found == nil {
1✔
622
                return ErrImageMetaNotFound
×
623
        }
×
624

625
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
626
        if err != nil {
1✔
UNCOV
627
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
UNCOV
628
        }
×
629

630
        // Image is in use, not allowed to delete
631
        if inUse {
2✔
632
                return ErrModelImageInActiveDeployment
1✔
633
        }
1✔
634

635
        // Delete image file (call to external service)
636
        // Noop for not existing file
637
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
638
        if err != nil {
1✔
639
                return err
×
UNCOV
640
        }
×
641
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
642
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
643
                return errors.Wrap(err, "Deleting image file")
×
644
        }
×
645

646
        // Delete metadata
647
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
648
                return errors.Wrap(err, "Deleting image metadata")
×
649
        }
×
650

651
        // update release
652
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
UNCOV
653
                return err
×
UNCOV
654
        }
×
655

656
        return nil
1✔
657
}
658

659
// ListImages according to specified filers.
660
func (d *Deployments) ListImages(
661
        ctx context.Context,
662
        filters *model.ReleaseOrImageFilter,
663
) ([]*model.Image, int, error) {
1✔
664
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
665
        if err != nil {
1✔
UNCOV
666
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
UNCOV
667
        }
×
668

669
        if imageList == nil {
2✔
670
                return make([]*model.Image, 0), 0, nil
1✔
671
        }
1✔
672

673
        return imageList, count, nil
1✔
674
}
675

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

×
680
        if err := constructor.Validate(); err != nil {
×
681
                return false, errors.Wrap(err, "Validating image metadata")
×
682
        }
×
683

684
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
685
        if err != nil {
×
686
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
UNCOV
687
        }
×
688

689
        if found {
×
690
                return false, ErrModelImageUsedInAnyDeployment
×
691
        }
×
692

693
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
694
        if err != nil {
×
695
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
UNCOV
696
        }
×
697

698
        if foundImage == nil {
×
699
                return false, nil
×
700
        }
×
701

702
        foundImage.SetModified(time.Now())
×
703
        foundImage.ImageMeta = constructor
×
UNCOV
704

×
705
        _, err = d.db.Update(ctx, foundImage)
×
706
        if err != nil {
×
707
                return false, errors.Wrap(err, "Updating image matadata")
×
UNCOV
708
        }
×
709

UNCOV
710
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
UNCOV
711
                return false, err
×
UNCOV
712
        }
×
713

UNCOV
714
        return true, nil
×
715
}
716

717
// DownloadLink presigned GET link to download image file.
718
// Returns error if image have not been uploaded.
719
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
720
        expire time.Duration) (*model.Link, error) {
1✔
721

1✔
722
        image, err := d.GetImage(ctx, imageID)
1✔
723
        if err != nil {
1✔
724
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
UNCOV
725
        }
×
726

727
        if image == nil {
1✔
728
                return nil, nil
×
729
        }
×
730

731
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
732
        if err != nil {
1✔
733
                return nil, err
×
734
        }
×
735
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
736
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
737
        if err != nil {
1✔
UNCOV
738
                return nil, errors.Wrap(err, "Searching for image file")
×
UNCOV
739
        }
×
740

741
        link, err := d.objectStorage.GetRequest(
1✔
742
                ctx,
1✔
743
                imagePath,
1✔
744
                image.Name+model.ArtifactFileSuffix,
1✔
745
                expire,
1✔
746
        )
1✔
747
        if err != nil {
1✔
UNCOV
748
                return nil, errors.Wrap(err, "Generating download link")
×
UNCOV
749
        }
×
750

751
        return link, nil
1✔
752
}
753

754
func (d *Deployments) UploadLink(
755
        ctx context.Context,
756
        expire time.Duration,
757
        skipVerify bool,
758
) (*model.UploadLink, error) {
6✔
759
        ctx, err := d.contextWithStorageSettings(ctx)
6✔
760
        if err != nil {
7✔
761
                return nil, err
1✔
762
        }
1✔
763

764
        artifactID := uuid.New().String()
5✔
765
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
5✔
766
        if skipVerify {
6✔
767
                path = model.ImagePathFromContext(ctx, artifactID)
1✔
768
        }
1✔
769
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
5✔
770
        if err != nil {
6✔
771
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
1✔
772
        }
1✔
773
        upLink := &model.UploadLink{
4✔
774
                ArtifactID: artifactID,
4✔
775
                IssuedAt:   time.Now(),
4✔
776
                Link:       *link,
4✔
777
        }
4✔
778
        err = d.db.InsertUploadIntent(ctx, upLink)
4✔
779
        if err != nil {
5✔
780
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
1✔
781
        }
1✔
782

783
        return upLink, err
3✔
784
}
785

786
func (d *Deployments) processUploadedArtifact(
787
        ctx context.Context,
788
        artifactID string,
789
        artifact io.ReadCloser,
790
        skipVerify bool,
791
) error {
5✔
792
        linkStatus := model.LinkStatusCompleted
5✔
793

5✔
794
        l := log.FromContext(ctx)
5✔
795
        defer artifact.Close()
5✔
796
        ctx, cancel := context.WithCancel(ctx)
5✔
797
        defer cancel()
5✔
798
        go func() { // Heatbeat routine
10✔
799
                ticker := time.NewTicker(inprogressIdleTime / 2)
5✔
800
                done := ctx.Done()
5✔
801
                defer ticker.Stop()
5✔
802
                for {
10✔
803
                        select {
5✔
804
                        case <-ticker.C:
×
805
                                err := d.db.UpdateUploadIntentStatus(
×
806
                                        ctx,
×
807
                                        artifactID,
×
808
                                        model.LinkStatusProcessing,
×
809
                                        model.LinkStatusProcessing,
×
810
                                )
×
UNCOV
811
                                if err != nil {
×
UNCOV
812
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
UNCOV
813
                                        cancel()
×
UNCOV
814
                                        return
×
UNCOV
815
                                }
×
816
                        case <-done:
5✔
817
                                return
5✔
818
                        }
819
                }
820
        }()
821
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
5✔
822
                ArtifactID:     artifactID,
5✔
823
                ArtifactReader: artifact,
5✔
824
        },
5✔
825
                skipVerify,
5✔
826
        )
5✔
827
        if err != nil {
9✔
828
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
4✔
829
                linkStatus = model.LinkStatusAborted
4✔
830
        }
4✔
831
        errDB := d.db.UpdateUploadIntentStatus(
5✔
832
                ctx, artifactID,
5✔
833
                model.LinkStatusProcessing, linkStatus,
5✔
834
        )
5✔
835
        if errDB != nil {
7✔
836
                l.Warnf("failed to update upload link status: %s", errDB)
2✔
837
        }
2✔
838
        return err
5✔
839
}
840

841
func (d *Deployments) CompleteUpload(
842
        ctx context.Context,
843
        intentID string,
844
        skipVerify bool,
845
) error {
10✔
846
        l := log.FromContext(ctx)
10✔
847
        idty := identity.FromContext(ctx)
10✔
848
        ctx, err := d.contextWithStorageSettings(ctx)
10✔
849
        if err != nil {
11✔
850
                return err
1✔
851
        }
1✔
852
        // Create an async context that doesn't cancel when server connection
853
        // closes.
854
        ctxAsync := context.Background()
9✔
855
        ctxAsync = log.WithContext(ctxAsync, l)
9✔
856
        ctxAsync = identity.WithContext(ctxAsync, idty)
9✔
857

9✔
858
        settings, _ := storage.SettingsFromContext(ctx)
9✔
859
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
9✔
860
        var artifactReader io.ReadCloser
9✔
861
        if skipVerify {
12✔
862
                artifactReader, err = d.objectStorage.GetObject(
3✔
863
                        ctxAsync,
3✔
864
                        model.ImagePathFromContext(ctx, intentID),
3✔
865
                )
3✔
866
        } else {
9✔
867
                artifactReader, err = d.objectStorage.GetObject(
6✔
868
                        ctxAsync,
6✔
869
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
6✔
870
                )
6✔
871
        }
6✔
872
        if err != nil {
11✔
873
                if errors.Is(err, storage.ErrObjectNotFound) {
3✔
874
                        return ErrUploadNotFound
1✔
875
                }
1✔
876
                return err
1✔
877
        }
878

879
        err = d.db.UpdateUploadIntentStatus(
7✔
880
                ctx,
7✔
881
                intentID,
7✔
882
                model.LinkStatusPending,
7✔
883
                model.LinkStatusProcessing,
7✔
884
        )
7✔
885
        if err != nil {
9✔
886
                errClose := artifactReader.Close()
2✔
887
                if errClose != nil {
3✔
888
                        l.Warnf("failed to close artifact reader: %s", errClose)
1✔
889
                }
1✔
890
                if errors.Is(err, store.ErrNotFound) {
3✔
891
                        return ErrUploadNotFound
1✔
892
                }
1✔
893
                return err
1✔
894
        }
895
        go d.processUploadedArtifact( // nolint:errcheck
5✔
896
                ctxAsync, intentID, artifactReader, skipVerify,
5✔
897
        )
5✔
898
        return nil
5✔
899
}
900

901
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
902
        return &model.ArtifactInfo{
1✔
903
                Format:  info.Format,
1✔
904
                Version: uint(info.Version),
1✔
905
        }
1✔
906
}
1✔
907

908
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
909
        var files []model.UpdateFile
1✔
910
        for _, u := range uFiles {
2✔
911
                files = append(files, model.UpdateFile{
1✔
912
                        Name:     u.Name,
1✔
913
                        Size:     u.Size,
1✔
914
                        Date:     &u.Date,
1✔
915
                        Checksum: string(u.Checksum),
1✔
916
                })
1✔
917
        }
1✔
918
        return files, nil
1✔
919
}
920

921
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
5✔
922
        metaArtifact := model.NewArtifactMeta()
5✔
923

5✔
924
        aReader := areader.NewReader(*r)
5✔
925

5✔
926
        // There is no signature verification here.
5✔
927
        // It is just simple check if artifact is signed or not.
5✔
928
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
5✔
UNCOV
929
                metaArtifact.Signed = true
×
UNCOV
930
                return nil
×
UNCOV
931
        }
×
932

933
        var err error
5✔
934
        if skipVerify {
8✔
935
                err = aReader.ReadArtifactHeaders()
3✔
936
                if err != nil {
5✔
937
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
938
                }
2✔
939
        } else {
3✔
940
                err = aReader.ReadArtifact()
3✔
941
                if err != nil {
6✔
942
                        return nil, errors.Wrap(err, "reading artifact error")
3✔
943
                }
3✔
944
        }
945

946
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
947
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
948

1✔
949
        metaArtifact.Name = aReader.GetArtifactName()
1✔
950
        if metaArtifact.Info.Version == 3 {
2✔
951
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
952
                if err != nil {
1✔
UNCOV
953
                        return nil, errors.Wrap(err,
×
954
                                "error parsing version 3 artifact")
×
955
                }
×
956

957
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
958
                if err != nil {
1✔
UNCOV
959
                        return nil, errors.Wrap(err,
×
UNCOV
960
                                "error parsing version 3 artifact")
×
UNCOV
961
                }
×
962

963
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
964
        }
965

966
        for _, p := range aReader.GetHandlers() {
2✔
967
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
968
                if err != nil {
1✔
969
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
970
                }
×
971

972
                uMetadata, err := p.GetUpdateMetaData()
1✔
973
                if err != nil {
1✔
UNCOV
974
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
UNCOV
975
                }
×
976

977
                metaArtifact.Updates = append(
1✔
978
                        metaArtifact.Updates,
1✔
979
                        model.Update{
1✔
980
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
981
                                        Type: p.GetUpdateType(),
1✔
982
                                },
1✔
983
                                Files:    uFiles,
1✔
984
                                MetaData: uMetadata,
1✔
985
                        })
1✔
986
        }
987

988
        return metaArtifact, nil
1✔
989
}
990

991
func getArtifactIDs(artifacts []*model.Image) []string {
7✔
992
        artifactIDs := make([]string, 0, len(artifacts))
7✔
993
        for _, artifact := range artifacts {
14✔
994
                artifactIDs = append(artifactIDs, artifact.Id)
7✔
995
        }
7✔
996
        return artifactIDs
7✔
997
}
998

999
// deployments
1000
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
4✔
1001
        ids := make([]string, len(devices))
4✔
1002
        for i, d := range devices {
8✔
1003
                ids[i] = d.ID
4✔
1004
        }
4✔
1005

1006
        return ids
4✔
1007
}
1008

1009
// updateDeploymentConstructor fills devices list with device ids
1010
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
1011
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
5✔
1012
        l := log.FromContext(ctx)
5✔
1013

5✔
1014
        id := identity.FromContext(ctx)
5✔
1015
        if id == nil {
5✔
UNCOV
1016
                l.Error("identity not present in the context")
×
UNCOV
1017
                return nil, ErrModelInternal
×
UNCOV
1018
        }
×
1019
        searchParams := model.SearchParams{
5✔
1020
                Page:    1,
5✔
1021
                PerPage: PerPageInventoryDevices,
5✔
1022
                Filters: []model.FilterPredicate{
5✔
1023
                        {
5✔
1024
                                Scope:     InventoryIdentityScope,
5✔
1025
                                Attribute: InventoryStatusAttributeName,
5✔
1026
                                Type:      "$eq",
5✔
1027
                                Value:     InventoryStatusAccepted,
5✔
1028
                        },
5✔
1029
                },
5✔
1030
        }
5✔
1031
        if len(constructor.Group) > 0 {
10✔
1032
                searchParams.Filters = append(
5✔
1033
                        searchParams.Filters,
5✔
1034
                        model.FilterPredicate{
5✔
1035
                                Scope:     InventoryGroupScope,
5✔
1036
                                Attribute: InventoryGroupAttributeName,
5✔
1037
                                Type:      "$eq",
5✔
1038
                                Value:     constructor.Group,
5✔
1039
                        })
5✔
1040
        }
5✔
1041

1042
        for {
11✔
1043
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
6✔
1044
                if err != nil {
7✔
1045
                        l.Errorf("error searching for devices")
1✔
1046
                        return nil, ErrModelInternal
1✔
1047
                }
1✔
1048
                if count < 1 {
6✔
1049
                        l.Errorf("no devices found")
1✔
1050
                        return nil, ErrNoDevices
1✔
1051
                }
1✔
1052
                if len(devices) < 1 {
4✔
UNCOV
1053
                        break
×
1054
                }
1055
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
4✔
1056
                if len(constructor.Devices) == count {
7✔
1057
                        break
3✔
1058
                }
1059
                searchParams.Page++
1✔
1060
        }
1061

1062
        return constructor, nil
3✔
1063
}
1064

1065
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1066
func (d *Deployments) CreateDeviceConfigurationDeployment(
1067
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1068
        deviceID, deploymentID string) (string, error) {
5✔
1069

5✔
1070
        if constructor == nil {
6✔
1071
                return "", ErrModelMissingInput
1✔
1072
        }
1✔
1073

1074
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
4✔
1075
                constructor,
4✔
1076
                deploymentID,
4✔
1077
        )
4✔
1078
        if err != nil {
4✔
UNCOV
1079
                return "", errors.Wrap(err, "failed to create deployment")
×
UNCOV
1080
        }
×
1081

1082
        deployment.DeviceList = []string{deviceID}
4✔
1083
        deployment.MaxDevices = 1
4✔
1084
        deployment.Configuration = []byte(constructor.Configuration)
4✔
1085
        deployment.Type = model.DeploymentTypeConfiguration
4✔
1086

4✔
1087
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
4✔
1088
        if err != nil {
5✔
1089
                return "", err
1✔
1090
        }
1✔
1091
        deployment.Groups = groups
3✔
1092

3✔
1093
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
5✔
1094
                if strings.Contains(err.Error(), "duplicate key error") {
3✔
1095
                        return "", ErrDuplicateDeployment
1✔
1096
                }
1✔
1097
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
3✔
1098
                        return "", ErrInvalidDeploymentID
1✔
1099
                }
1✔
1100
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1101
        }
1102

1103
        return deployment.Id, nil
2✔
1104
}
1105

1106
// CreateDeployment precomputes new deployment and schedules it for devices.
1107
func (d *Deployments) CreateDeployment(ctx context.Context,
1108
        constructor *model.DeploymentConstructor) (string, error) {
9✔
1109

9✔
1110
        var err error
9✔
1111

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

1116
        if err := constructor.Validate(); err != nil {
8✔
UNCOV
1117
                return "", errors.Wrap(err, "Validating deployment")
×
UNCOV
1118
        }
×
1119

1120
        if len(constructor.Group) > 0 || constructor.AllDevices {
13✔
1121
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
5✔
1122
                if err != nil {
7✔
1123
                        return "", err
2✔
1124
                }
2✔
1125
        }
1126

1127
        deployment, err := model.NewDeploymentFromConstructor(constructor)
6✔
1128
        if err != nil {
6✔
UNCOV
1129
                return "", errors.Wrap(err, "failed to create deployment")
×
UNCOV
1130
        }
×
1131

1132
        // Assign artifacts to the deployment.
1133
        // When new artifact(s) with the artifact name same as the one in the deployment
1134
        // will be uploaded to the backend, it will also become part of this deployment.
1135
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
6✔
1136
        if err != nil {
6✔
UNCOV
1137
                return "", errors.Wrap(err, "Finding artifact with given name")
×
UNCOV
1138
        }
×
1139

1140
        if len(artifacts) == 0 {
7✔
1141
                return "", ErrNoArtifact
1✔
1142
        }
1✔
1143

1144
        deployment.Artifacts = getArtifactIDs(artifacts)
6✔
1145
        deployment.DeviceList = constructor.Devices
6✔
1146
        deployment.MaxDevices = len(constructor.Devices)
6✔
1147
        deployment.Type = model.DeploymentTypeSoftware
6✔
1148
        if len(constructor.Group) > 0 {
9✔
1149
                deployment.Groups = []string{constructor.Group}
3✔
1150
        }
3✔
1151

1152
        // single device deployment case
1153
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
9✔
1154
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
3✔
1155
                if err != nil {
3✔
UNCOV
1156
                        return "", err
×
UNCOV
1157
                }
×
1158
                deployment.Groups = groups
3✔
1159
        }
1160

1161
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
7✔
1162
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1163
        }
1✔
1164

1165
        return deployment.Id, nil
5✔
1166
}
1167

1168
func (d *Deployments) getDeploymentGroups(
1169
        ctx context.Context,
1170
        devices []string,
1171
) ([]string, error) {
6✔
1172
        id := identity.FromContext(ctx)
6✔
1173

6✔
1174
        //only for single device deployment case
6✔
1175
        if len(devices) != 1 {
6✔
UNCOV
1176
                return nil, nil
×
UNCOV
1177
        }
×
1178

1179
        if id == nil {
7✔
1180
                id = &identity.Identity{}
1✔
1181
        }
1✔
1182

1183
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
6✔
1184
        if err != nil && err != inventory.ErrDevNotFound {
7✔
1185
                return nil, err
1✔
1186
        }
1✔
1187
        return groups, nil
5✔
1188
}
1189

1190
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1191
func (d *Deployments) IsDeploymentFinished(
1192
        ctx context.Context,
1193
        deploymentID string,
1194
) (bool, error) {
1✔
1195
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1196
        if err != nil {
1✔
UNCOV
1197
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
UNCOV
1198
        }
×
1199
        if deployment == nil {
2✔
1200
                return true, nil
1✔
1201
        }
1✔
1202

1203
        return false, nil
1✔
1204
}
1205

1206
// GetDeployment fetches deployment by ID
1207
func (d *Deployments) GetDeployment(ctx context.Context,
1208
        deploymentID string) (*model.Deployment, error) {
1✔
1209

1✔
1210
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1211
        if err != nil {
1✔
1212
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
UNCOV
1213
        }
×
1214

1215
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
UNCOV
1216
                return nil, err
×
UNCOV
1217
        }
×
1218

1219
        return deployment, nil
1✔
1220
}
1221

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

4✔
1227
        var found bool
4✔
1228

4✔
1229
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
4✔
1230
        if err != nil {
5✔
1231
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1232
        }
1✔
1233

1234
        if found {
4✔
1235
                return found, nil
1✔
1236
        }
1✔
1237

1238
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
3✔
1239
                imageID, model.ActiveDeploymentStatuses()...)
3✔
1240
        if err != nil {
4✔
1241
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1242
        }
1✔
1243

1244
        return found, nil
2✔
1245
}
1246

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

×
1251
        var found bool
×
UNCOV
1252

×
1253
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1254
        if err != nil {
×
1255
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
UNCOV
1256
        }
×
1257

1258
        if found {
×
1259
                return found, nil
×
1260
        }
×
1261

1262
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
UNCOV
1263
        if err != nil {
×
UNCOV
1264
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
UNCOV
1265
        }
×
1266

UNCOV
1267
        return found, nil
×
1268
}
1269

1270
// Retrieves the model.Deployment and model.DeviceDeployment structures
1271
// for the device. Upon error, nil is returned for both deployment structures.
1272
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1273
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1274

2✔
1275
        // Retrieve device deployment
2✔
1276
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
2✔
1277

2✔
1278
        if err != nil {
2✔
UNCOV
1279
                return nil, nil, errors.Wrap(err,
×
UNCOV
1280
                        "Searching for oldest active deployment for the device")
×
1281
        } else if deviceDeployment == nil {
3✔
1282
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1283
        }
1✔
1284

1285
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1286
        if err != nil {
2✔
UNCOV
1287
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
UNCOV
1288
        }
×
1289
        if deployment == nil {
2✔
UNCOV
1290
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
UNCOV
1291
        }
×
1292

1293
        return deployment, deviceDeployment, nil
2✔
1294
}
1295

1296
// getNewDeploymentForDevice returns deployment object and creates and returns
1297
// new device deployment for the device;
1298
//
1299
// we are interested only in the deployments that are newer than the latest
1300
// deployment applied by the device;
1301
// this way we guarantee that the device will not receive deployment
1302
// that is older than the one installed on the device;
1303
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1304
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1305

1✔
1306
        var lastDeployment *time.Time
1✔
1307
        //get latest device deployment for the device;
1✔
1308
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1309
        if err != nil {
1✔
UNCOV
1310
                return nil, nil, errors.Wrap(err,
×
UNCOV
1311
                        "Searching for latest active deployment for the device")
×
1312
        } else if deviceDeployment == nil {
2✔
1313
                lastDeployment = &time.Time{}
1✔
1314
        } else {
2✔
1315
                lastDeployment = deviceDeployment.Created
1✔
1316
        }
1✔
1317

1318
        //get deployments newer then last device deployment
1319
        //iterate over deployments and check if the device is part of the deployment or not
1320
        for skip := 0; true; skip += 100 {
2✔
1321
                deployments, err := d.db.FindNewerActiveDeployments(ctx, lastDeployment, skip, 100)
1✔
1322
                if err != nil {
1✔
UNCOV
1323
                        return nil, nil, errors.Wrap(err,
×
UNCOV
1324
                                "Failed to search for newer active deployments")
×
UNCOV
1325
                }
×
1326
                if len(deployments) == 0 {
2✔
1327
                        return nil, nil, nil
1✔
1328
                }
1✔
1329

1330
                for _, deployment := range deployments {
2✔
1331
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1332
                        if err != nil {
1✔
UNCOV
1333
                                return nil, nil, err
×
1334
                        }
×
1335
                        if ok {
2✔
1336
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1337
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1338
                                if err != nil {
1✔
UNCOV
1339
                                        return nil, nil, err
×
UNCOV
1340
                                }
×
1341
                                return deployment, deviceDeployment, nil
1✔
1342
                        }
1343
                }
1344
        }
1345

UNCOV
1346
        return nil, nil, nil
×
1347
}
1348

1349
func (d *Deployments) createDeviceDeploymentWithStatus(
1350
        ctx context.Context, deviceID string,
1351
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1352
) (*model.DeviceDeployment, error) {
6✔
1353
        prevStatus := model.DeviceDeploymentStatusNull
6✔
1354
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
6✔
1355
        if err != nil && err != mongo.ErrStorageNotFound {
6✔
UNCOV
1356
                return nil, err
×
1357
        } else if deviceDeployment != nil {
6✔
UNCOV
1358
                prevStatus = deviceDeployment.Status
×
UNCOV
1359
        }
×
1360

1361
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
6✔
1362
        deviceDeployment.Status = status
6✔
1363
        deviceDeployment.Active = status.Active()
6✔
1364
        deviceDeployment.Created = deployment.Created
6✔
1365

6✔
1366
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
6✔
1367
                return nil, err
×
1368
        }
×
1369

1370
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
6✔
1371
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
6✔
UNCOV
1372
                return nil, err
×
UNCOV
1373
        }
×
1374

1375
        // after inserting new device deployment update deployment stats
1376
        // in the database and locally, and update deployment status
1377
        if err := d.db.UpdateStatsInc(
6✔
1378
                ctx, deployment.Id,
6✔
1379
                prevStatus, status,
6✔
1380
        ); err != nil {
6✔
UNCOV
1381
                return nil, err
×
UNCOV
1382
        }
×
1383

1384
        deployment.Stats.Inc(status)
6✔
1385

6✔
1386
        err = d.recalcDeploymentStatus(ctx, deployment)
6✔
1387
        if err != nil {
6✔
UNCOV
1388
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1389
        }
×
1390

1391
        if !status.Active() {
11✔
1392
                err := d.reindexDevice(ctx, deviceID)
5✔
1393
                if err != nil {
5✔
1394
                        l := log.FromContext(ctx)
×
1395
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1396
                }
×
1397
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1398
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
UNCOV
1399
                        l := log.FromContext(ctx)
×
UNCOV
1400
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
1401
                }
×
1402
        }
1403

1404
        return deviceDeployment, nil
6✔
1405
}
1406

1407
func (d *Deployments) isDevicePartOfDeployment(
1408
        ctx context.Context,
1409
        deviceID string,
1410
        deployment *model.Deployment,
1411
) (bool, error) {
8✔
1412
        for _, id := range deployment.DeviceList {
14✔
1413
                if id == deviceID {
12✔
1414
                        return true, nil
6✔
1415
                }
6✔
1416
        }
1417
        return false, nil
3✔
1418
}
1419

1420
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1421
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1422
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
2✔
1423

2✔
1424
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
2✔
1425
        if err != nil {
2✔
UNCOV
1426
                return nil, ErrModelInternal
×
1427
        } else if deployment == nil {
3✔
1428
                return nil, nil
1✔
1429
        }
1✔
1430

1431
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
2✔
1432
        if err != nil {
3✔
1433
                return nil, err
1✔
1434
        }
1✔
1435
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
2✔
1436
}
1437

1438
func (d *Deployments) getDeploymentInstructions(
1439
        ctx context.Context,
1440
        deployment *model.Deployment,
1441
        deviceDeployment *model.DeviceDeployment,
1442
        request *model.DeploymentNextRequest,
1443
) (*model.DeploymentInstructions, error) {
2✔
1444

2✔
1445
        var newArtifactAssigned bool
2✔
1446

2✔
1447
        l := log.FromContext(ctx)
2✔
1448

2✔
1449
        if deployment.Type == model.DeploymentTypeConfiguration {
3✔
1450
                // There's nothing more we need to do, the link must be filled
1✔
1451
                // in by the API layer.
1✔
1452
                return &model.DeploymentInstructions{
1✔
1453
                        ID: deployment.Id,
1✔
1454
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1455
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1456
                                // use deployment ID togheter with device ID as artifact ID
1✔
1457
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1458
                                ArtifactName:          deployment.ArtifactName,
1✔
1459
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1460
                        },
1✔
1461
                        Type: model.DeploymentTypeConfiguration,
1✔
1462
                }, nil
1✔
1463
        }
1✔
1464

1465
        // assing artifact to the device deployment
1466
        // only if it was not assgined previously
1467
        if deviceDeployment.Image == nil {
4✔
1468
                if err := d.assignArtifact(
2✔
1469
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
2✔
UNCOV
1470
                        return nil, err
×
1471
                }
×
1472
                newArtifactAssigned = true
2✔
1473
        }
1474

1475
        if deviceDeployment.Image == nil {
2✔
UNCOV
1476
                // No artifact - return empty response
×
UNCOV
1477
                return nil, nil
×
UNCOV
1478
        }
×
1479

1480
        // if the deployment is not forcing the installation, and
1481
        // if artifact was recognized as already installed, and this is
1482
        // a new device deployment - indicated by device deployment status "pending",
1483
        // handle already installed artifact case
1484
        if !deployment.ForceInstallation &&
2✔
1485
                d.isAlreadyInstalled(request, deviceDeployment) &&
2✔
1486
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
4✔
1487
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
2✔
1488
        }
2✔
1489

1490
        // if new artifact has been assigned to device deployment
1491
        // add artifact size to deployment total size,
1492
        // before returning deployment instruction to the device
1493
        if newArtifactAssigned {
2✔
1494
                if err := d.db.IncrementDeploymentTotalSize(
1✔
1495
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
UNCOV
1496
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1497
                }
×
1498
        }
1499

1500
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1501
        if err != nil {
1✔
UNCOV
1502
                return nil, err
×
UNCOV
1503
        }
×
1504

1505
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1506
        link, err := d.objectStorage.GetRequest(
1✔
1507
                ctx,
1✔
1508
                imagePath,
1✔
1509
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1510
                DefaultUpdateDownloadLinkExpire,
1✔
1511
        )
1✔
1512
        if err != nil {
1✔
UNCOV
1513
                return nil, errors.Wrap(err, "Generating download link for the device")
×
UNCOV
1514
        }
×
1515

1516
        instructions := &model.DeploymentInstructions{
1✔
1517
                ID: deviceDeployment.DeploymentId,
1✔
1518
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1519
                        ID: deviceDeployment.Image.Id,
1✔
1520
                        ArtifactName: deviceDeployment.Image.
1✔
1521
                                ArtifactMeta.Name,
1✔
1522
                        Source: *link,
1✔
1523
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1524
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1525
                },
1✔
1526
        }
1✔
1527

1✔
1528
        return instructions, nil
1✔
1529
}
1530

1531
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1532
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
2✔
1533
        if deviceDeployment.Request != nil {
3✔
1534
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1535
                        // the device reported different device type and/or artifact name
1✔
1536
                        // during the update process, which should never happen;
1✔
1537
                        // mark deployment for this device as failed to force client to rollback
1✔
1538
                        l := log.FromContext(ctx)
1✔
1539
                        l.Errorf(
1✔
1540
                                "Device with id %s reported new data: %s during update process;"+
1✔
1541
                                        "old data: %s",
1✔
1542
                                deviceID, request, deviceDeployment.Request)
1✔
1543

1✔
1544
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1545
                                model.DeviceDeploymentState{
1✔
1546
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1547
                                }); err != nil {
1✔
UNCOV
1548
                                return errors.Wrap(err, "Failed to update deployment status")
×
UNCOV
1549
                        }
×
1550
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1551
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1552
                        }
×
1553
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1554
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
UNCOV
1555
                                l := log.FromContext(ctx)
×
UNCOV
1556
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
1557
                        }
×
1558
                        return ErrConflictingRequestData
1✔
1559
                }
1560
        } else {
2✔
1561
                // save the request
2✔
1562
                if err := d.db.SaveDeviceDeploymentRequest(
2✔
1563
                        ctx, deviceDeployment.Id, request); err != nil {
2✔
UNCOV
1564
                        return err
×
UNCOV
1565
                }
×
1566
        }
1567
        return nil
2✔
1568
}
1569

1570
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1571
// ID `deviceID`. Returns nil if update was successful.
1572
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1573
        deviceID string, ddState model.DeviceDeploymentState) error {
6✔
1574

6✔
1575
        l := log.FromContext(ctx)
6✔
1576

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

6✔
1579
        var finishTime *time.Time = nil
6✔
1580
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
10✔
1581
                now := time.Now()
4✔
1582
                finishTime = &now
4✔
1583
        }
4✔
1584

1585
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
6✔
1586
        if err == mongo.ErrStorageNotFound {
7✔
1587
                return ErrStorageNotFound
1✔
1588
        } else if err != nil {
6✔
UNCOV
1589
                return err
×
1590
        }
×
1591

1592
        currentStatus := dd.Status
5✔
1593

5✔
1594
        if currentStatus == model.DeviceDeploymentStatusAborted {
5✔
1595
                return ErrDeploymentAborted
×
UNCOV
1596
        }
×
1597

1598
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
5✔
1599
                return ErrDeviceDecommissioned
×
1600
        }
×
1601

1602
        // nothing to do
1603
        if ddState.Status == currentStatus {
5✔
UNCOV
1604
                return nil
×
UNCOV
1605
        }
×
1606

1607
        // update finish time
1608
        ddState.FinishTime = finishTime
5✔
1609

5✔
1610
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
5✔
1611
                deviceID, deploymentID, ddState)
5✔
1612
        if err != nil {
5✔
1613
                return err
×
UNCOV
1614
        }
×
1615

1616
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
5✔
UNCOV
1617
                return err
×
1618
        }
×
1619

1620
        // fetch deployment stats and update deployment status
1621
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
5✔
1622
        if err != nil {
5✔
1623
                return errors.Wrap(err, "failed when searching for deployment")
×
1624
        }
×
1625

1626
        err = d.recalcDeploymentStatus(ctx, deployment)
5✔
1627
        if err != nil {
5✔
UNCOV
1628
                return errors.Wrap(err, "failed to update deployment status")
×
UNCOV
1629
        }
×
1630

1631
        if !ddState.Status.Active() {
9✔
1632
                l := log.FromContext(ctx)
4✔
1633
                ldd := model.DeviceDeployment{
4✔
1634
                        DeviceId:     dd.DeviceId,
4✔
1635
                        DeploymentId: dd.DeploymentId,
4✔
1636
                        Id:           dd.Id,
4✔
1637
                        Status:       ddState.Status,
4✔
1638
                }
4✔
1639
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
4✔
UNCOV
1640
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1641
                }
×
1642
                if err := d.reindexDevice(ctx, deviceID); err != nil {
4✔
UNCOV
1643
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
1644
                }
×
1645
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
4✔
UNCOV
1646
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
1647
                }
×
1648
        }
1649

1650
        return nil
5✔
1651
}
1652

1653
// recalcDeploymentStatus inspects the deployment stats and
1654
// recalculates and updates its status
1655
// it should be used whenever deployment stats are touched
1656
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
10✔
1657
        status := dep.GetStatus()
10✔
1658

10✔
1659
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
10✔
UNCOV
1660
                return err
×
UNCOV
1661
        }
×
1662

1663
        return nil
10✔
1664
}
1665

1666
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1667
        deploymentID string) (model.Stats, error) {
1✔
1668

1✔
1669
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1670

1✔
1671
        if err != nil {
1✔
1672
                return nil, errors.Wrap(err, "checking deployment id")
×
UNCOV
1673
        }
×
1674

1675
        if deployment == nil {
1✔
UNCOV
1676
                return nil, nil
×
1677
        }
×
1678

1679
        return deployment.Stats, nil
1✔
1680
}
1681
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1682
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1683

×
UNCOV
1684
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1685

×
1686
        if err != nil {
×
1687
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
UNCOV
1688
        }
×
1689

UNCOV
1690
        if deploymentStats == nil {
×
UNCOV
1691
                return nil, ErrModelDeploymentNotFound
×
UNCOV
1692
        }
×
1693

UNCOV
1694
        return deploymentStats, nil
×
1695
}
1696

1697
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1698
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1699
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1700

1✔
1701
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1702
        if err != nil {
1✔
1703
                return nil, ErrModelInternal
×
UNCOV
1704
        }
×
1705

1706
        if deployment == nil {
1✔
1707
                return nil, ErrModelDeploymentNotFound
×
1708
        }
×
1709

1710
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1711
        if err != nil {
1✔
UNCOV
1712
                return nil, ErrModelInternal
×
UNCOV
1713
        }
×
1714

1715
        return statuses, nil
1✔
1716
}
1717

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

1✔
1721
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1722
        if err != nil {
1✔
1723
                return nil, -1, ErrModelInternal
×
UNCOV
1724
        }
×
1725

1726
        if deployment == nil {
1✔
1727
                return nil, -1, ErrModelDeploymentNotFound
×
1728
        }
×
1729

1730
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1731
        if err != nil {
1✔
UNCOV
1732
                return nil, -1, ErrModelInternal
×
UNCOV
1733
        }
×
1734

1735
        return statuses, totalCount, nil
1✔
1736
}
1737

1738
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1739
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
4✔
1740
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
4✔
1741
        if err != nil {
5✔
1742
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1743
        }
1✔
1744

1745
        deploymentIDs := make([]string, len(deviceDeployments))
3✔
1746
        for i, deviceDeployment := range deviceDeployments {
9✔
1747
                deploymentIDs[i] = deviceDeployment.DeploymentId
6✔
1748
        }
6✔
1749

1750
        deployments, _, err := d.db.Find(ctx, model.Query{
3✔
1751
                IDs:          deploymentIDs,
3✔
1752
                Limit:        len(deviceDeployments),
3✔
1753
                DisableCount: true,
3✔
1754
        })
3✔
1755
        if err != nil {
4✔
1756
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1757
        }
1✔
1758

1759
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1760
        for _, deployment := range deployments {
5✔
1761
                deploymentsMap[deployment.Id] = deployment
3✔
1762
        }
3✔
1763

1764
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1765
        for i, deviceDeployment := range deviceDeployments {
6✔
1766
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
7✔
1767
                        res = append(res, model.DeviceDeploymentListItem{
3✔
1768
                                Id:         deviceDeployment.Id,
3✔
1769
                                Deployment: deployment,
3✔
1770
                                Device:     &deviceDeployments[i],
3✔
1771
                        })
3✔
1772
                } else {
4✔
1773
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1774
                                Id:     deviceDeployment.Id,
1✔
1775
                                Device: &deviceDeployments[i],
1✔
1776
                        })
1✔
1777
                }
1✔
1778
        }
1779

1780
        return res, totalCount, nil
2✔
1781
}
1782

1783
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1784
        ctx context.Context,
1785
        deployment *model.Deployment,
1786
) error {
6✔
1787
        if deployment.DeviceCount == nil {
6✔
1788
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1789
                if err != nil {
×
1790
                        return errors.Wrap(err, "counting device deployments")
×
1791
                }
×
UNCOV
1792
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
UNCOV
1793
                if err != nil {
×
UNCOV
1794
                        return errors.Wrap(err, "setting the device count for the deployment")
×
UNCOV
1795
                }
×
UNCOV
1796
                deployment.DeviceCount = &deviceCount
×
1797
        }
1798

1799
        return nil
6✔
1800
}
1801

1802
func (d *Deployments) LookupDeployment(ctx context.Context,
1803
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1804
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1805

1✔
1806
        if err != nil {
1✔
UNCOV
1807
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
UNCOV
1808
        }
×
1809

1810
        if list == nil {
2✔
1811
                return make([]*model.Deployment, 0), 0, nil
1✔
1812
        }
1✔
1813

UNCOV
1814
        for _, deployment := range list {
×
1815
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
UNCOV
1816
                        return nil, 0, err
×
UNCOV
1817
                }
×
1818
        }
1819

UNCOV
1820
        return list, totalCount, nil
×
1821
}
1822

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

1✔
1828
        // repack to temporary deployment log and validate
1✔
1829
        dlog := model.DeploymentLog{
1✔
1830
                DeviceID:     deviceID,
1✔
1831
                DeploymentID: deploymentID,
1✔
1832
                Messages:     logs,
1✔
1833
        }
1✔
1834
        if err := dlog.Validate(); err != nil {
1✔
1835
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1836
        }
×
1837

1838
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
UNCOV
1839
                if err != nil {
×
UNCOV
1840
                        return err
×
UNCOV
1841
                } else {
×
1842
                        return ErrModelDeploymentNotFound
×
1843
                }
×
1844
        }
1845

1846
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
UNCOV
1847
                return err
×
UNCOV
1848
        }
×
1849

1850
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1851
                deviceID, deploymentID, true)
1✔
1852
}
1853

1854
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1855
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1856

1✔
1857
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1858
                deviceID, deploymentID)
1✔
1859
}
1✔
1860

1861
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1862
        deploymentID string, deviceID string) (bool, error) {
1✔
1863
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1864
}
1✔
1865

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

5✔
1869
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
6✔
1870
                return err
1✔
1871
        }
1✔
1872

1873
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
4✔
1874
                ctx, deploymentID)
4✔
1875
        if err != nil {
5✔
1876
                return err
1✔
1877
        }
1✔
1878

1879
        // update statistics
1880
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
4✔
1881
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1882
        }
1✔
1883

1884
        // when aborting the deployment we need to set status directly instead of
1885
        // using recalcDeploymentStatus method;
1886
        // it is possible that the deployment does not have any device deployments yet;
1887
        // in that case, all statistics are 0 and calculating status based on statistics
1888
        // will not work - the calculated status will be "pending"
1889
        if err := d.db.SetDeploymentStatus(ctx,
2✔
1890
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
2✔
UNCOV
1891
                return errors.Wrap(err, "failed to update deployment status")
×
UNCOV
1892
        }
×
1893

1894
        return nil
2✔
1895
}
1896

1897
func (d *Deployments) updateDeviceDeploymentsStatus(
1898
        ctx context.Context,
1899
        deviceId string,
1900
        status model.DeviceDeploymentStatus,
1901
) error {
15✔
1902
        var latestDeployment *time.Time
15✔
1903
        // Retrieve active device deployment for the device
15✔
1904
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
15✔
1905
        if err != nil {
17✔
1906
                return errors.Wrap(err, "Searching for active deployment for the device")
2✔
1907
        } else if deviceDeployment != nil {
17✔
1908
                now := time.Now()
2✔
1909
                ddStatus := model.DeviceDeploymentState{
2✔
1910
                        Status:     status,
2✔
1911
                        FinishTime: &now,
2✔
1912
                }
2✔
1913
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
2✔
1914
                        deviceId, ddStatus); err != nil {
2✔
UNCOV
1915
                        return errors.Wrap(err, "updating device deployment status")
×
UNCOV
1916
                }
×
1917
                latestDeployment = deviceDeployment.Created
2✔
1918
        } else {
11✔
1919
                // get latest device deployment for the device
11✔
1920
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
11✔
1921
                if err != nil {
11✔
UNCOV
1922
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1923
                } else if deviceDeployment == nil {
20✔
1924
                        latestDeployment = &time.Time{}
9✔
1925
                } else {
11✔
1926
                        latestDeployment = deviceDeployment.Created
2✔
1927
                }
2✔
1928
        }
1929

1930
        // get deployments newer then last device deployment
1931
        // iterate over deployments and check if the device is part of the deployment or not
1932
        // if the device is part of the deployment create new, decommisioned device deployment
1933
        for skip := 0; true; skip += 100 {
33✔
1934
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
20✔
1935
                if err != nil {
20✔
UNCOV
1936
                        return errors.Wrap(err, "Failed to search for newer active deployments")
×
UNCOV
1937
                }
×
1938
                if len(deployments) == 0 {
33✔
1939
                        break
13✔
1940
                }
1941
                for _, deployment := range deployments {
14✔
1942
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
7✔
1943
                        if err != nil {
7✔
UNCOV
1944
                                return err
×
1945
                        }
×
1946
                        if ok {
12✔
1947
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
5✔
1948
                                        deviceId, deployment, status)
5✔
1949
                                if err != nil {
5✔
1950
                                        return err
×
1951
                                }
×
1952
                                if !status.Active() {
10✔
1953
                                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1954
                                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
UNCOV
1955
                                                l := log.FromContext(ctx)
×
UNCOV
1956
                                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
1957
                                        }
×
1958
                                }
1959
                        }
1960
                }
1961
        }
1962

1963
        if err := d.reindexDevice(ctx, deviceId); err != nil {
13✔
UNCOV
1964
                l := log.FromContext(ctx)
×
UNCOV
1965
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
UNCOV
1966
        }
×
1967

1968
        return nil
13✔
1969
}
1970

1971
// DecommissionDevice updates the status of all the pending and active deployments for a device
1972
// to decommissioned
1973
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
7✔
1974
        return d.updateDeviceDeploymentsStatus(
7✔
1975
                ctx,
7✔
1976
                deviceId,
7✔
1977
                model.DeviceDeploymentStatusDecommissioned,
7✔
1978
        )
7✔
1979
}
7✔
1980

1981
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1982
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
8✔
1983
        return d.updateDeviceDeploymentsStatus(
8✔
1984
                ctx,
8✔
1985
                deviceId,
8✔
1986
                model.DeviceDeploymentStatusAborted,
8✔
1987
        )
8✔
1988
}
8✔
1989

1990
// DeleteDeviceDeploymentsHistory deletes the device deployments history
1991
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
2✔
1992
        // get device deployments which will be marked as deleted
2✔
1993
        f := false
2✔
1994
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
2✔
1995
        if err != nil {
2✔
1996
                return err
×
1997
        }
×
1998

1999
        // no device deployments to update
2000
        if len(dd) <= 0 {
2✔
UNCOV
2001
                return nil
×
UNCOV
2002
        }
×
2003

2004
        // mark device deployments as deleted
2005
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
3✔
2006
                return err
1✔
2007
        }
1✔
2008

2009
        // trigger reindexing of updated device deployments
2010
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2011
        for i, d := range dd {
2✔
2012
                deviceDeployments[i].ID = d.Id
1✔
2013
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2014
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2015
        }
1✔
2016
        if d.reportingClient != nil {
2✔
2017
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2018
        }
1✔
2019
        return err
1✔
2020
}
2021

2022
// Storage settings
2023
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
2024
        settings, err := d.db.GetStorageSettings(ctx)
3✔
2025
        if err != nil {
4✔
2026
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2027
        }
1✔
2028

2029
        return settings, nil
2✔
2030
}
2031

2032
func (d *Deployments) SetStorageSettings(
2033
        ctx context.Context,
2034
        storageSettings *model.StorageSettings,
2035
) error {
4✔
2036
        if storageSettings != nil {
8✔
2037
                ctx = storage.SettingsWithContext(ctx, storageSettings)
4✔
2038
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
4✔
UNCOV
2039
                        return errors.WithMessage(err,
×
UNCOV
2040
                                "the provided storage settings failed the health check",
×
UNCOV
2041
                        )
×
UNCOV
2042
                }
×
2043
        }
2044
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
6✔
2045
                return errors.Wrap(err, "Failed to save settings")
2✔
2046
        }
2✔
2047

2048
        return nil
2✔
2049
}
2050

2051
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
8✔
2052
        d.reportingClient = c
8✔
2053
        return d
8✔
2054
}
8✔
2055

2056
func (d *Deployments) haveReporting() bool {
6✔
2057
        return d.reportingClient != nil
6✔
2058
}
6✔
2059

2060
func (d *Deployments) search(
2061
        ctx context.Context,
2062
        tid string,
2063
        parms model.SearchParams,
2064
) ([]model.InvDevice, int, error) {
6✔
2065
        if d.haveReporting() {
7✔
2066
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2067
        } else {
6✔
2068
                return d.inventoryClient.Search(ctx, tid, parms)
5✔
2069
        }
5✔
2070
}
2071

2072
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2073
        ctx context.Context,
2074
        artifactName string,
2075
) error {
2✔
2076
        // first check if there are pending deployments with given artifact name
2✔
2077
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
2✔
2078
        if err != nil {
2✔
UNCOV
2079
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
UNCOV
2080
        }
×
2081
        if !exists {
3✔
2082
                return nil
1✔
2083
        }
1✔
2084

2085
        // Assign artifacts to the deployments with given artifact name
2086
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2087
        if err != nil {
1✔
2088
                return errors.Wrap(err, "Finding artifact with given name")
×
UNCOV
2089
        }
×
2090

2091
        if len(artifacts) == 0 {
1✔
UNCOV
2092
                return ErrNoArtifact
×
UNCOV
2093
        }
×
2094
        artifactIDs := getArtifactIDs(artifacts)
1✔
2095
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2096
}
2097

2098
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
25✔
2099
        if d.reportingClient != nil {
28✔
2100
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
3✔
2101
        }
3✔
2102
        return nil
22✔
2103
}
2104

2105
func (d *Deployments) reindexDeployment(ctx context.Context,
2106
        deviceID, deploymentID, ID string) error {
17✔
2107
        if d.reportingClient != nil {
20✔
2108
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
3✔
2109
        }
3✔
2110
        return nil
14✔
2111
}
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