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

mendersoftware / deployments / 731956009

pending completion
731956009

Pull #809

gitlab-ci

Fabio Tranchitella
feat: add support to filter device deployments by status `finished`
Pull Request #809: MEN-5911: feat: internal end-point to retrieve the device deployments history

95 of 131 new or added lines in 7 files covered. (72.52%)

1 existing line in 1 file now uncovered.

6112 of 7840 relevant lines covered (77.96%)

77.37 hits per line

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

74.64
/app/app.go
1
// Copyright 2022 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
        "reflect"
23
        "strings"
24
        "time"
25

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

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

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

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

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

60
var (
61
        ArtifactConfigureType = "mender-configure"
62
)
63

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

79
        ErrMsgArtifactConflict = "An artifact with the same name has conflicting dependencies"
80

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

97
//deployments
98

99
//go:generate ../utils/mockgen.sh
100
type App interface {
101
        HealthCheck(ctx context.Context) error
102
        // limits
103
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
104
        ProvisionTenant(ctx context.Context, tenant_id string) error
105

106
        // Storage Settings
107
        GetStorageSettings(ctx context.Context) (*model.StorageSettings, error)
108
        SetStorageSettings(ctx context.Context, storageSettings *model.StorageSettings) error
109

110
        // images
111
        ListImages(
112
                ctx context.Context,
113
                filters *model.ReleaseOrImageFilter,
114
        ) ([]*model.Image, int, error)
115
        DownloadLink(ctx context.Context, imageID string,
116
                expire time.Duration) (*model.Link, error)
117
        GetImage(ctx context.Context, id string) (*model.Image, error)
118
        DeleteImage(ctx context.Context, imageID string) error
119
        CreateImage(ctx context.Context,
120
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
121
        GenerateImage(ctx context.Context,
122
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
123
        GenerateConfigurationImage(
124
                ctx context.Context,
125
                deviceType string,
126
                deploymentID string,
127
        ) (io.Reader, error)
128
        EditImage(ctx context.Context, id string,
129
                constructorData *model.ImageMeta) (bool, error)
130

131
        // deployments
132
        CreateDeployment(ctx context.Context,
133
                constructor *model.DeploymentConstructor) (string, error)
134
        GetDeployment(ctx context.Context, deploymentID string) (*model.Deployment, error)
135
        IsDeploymentFinished(ctx context.Context, deploymentID string) (bool, error)
136
        AbortDeployment(ctx context.Context, deploymentID string) error
137
        GetDeploymentStats(ctx context.Context, deploymentID string) (model.Stats, error)
138
        GetDeploymentsStats(ctx context.Context,
139
                deploymentIDs ...string) ([]*model.DeploymentStats, error)
140
        GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
141
                request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error)
142
        HasDeploymentForDevice(ctx context.Context, deploymentID string,
143
                deviceID string) (bool, error)
144
        UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
145
                deviceID string, state model.DeviceDeploymentState) error
146
        GetDeviceStatusesForDeployment(ctx context.Context,
147
                deploymentID string) ([]model.DeviceDeployment, error)
148
        GetDevicesListForDeployment(ctx context.Context,
149
                query store.ListQuery) ([]model.DeviceDeployment, int, error)
150
        GetDeviceDeploymentListForDevice(ctx context.Context,
151
                query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error)
152
        LookupDeployment(ctx context.Context,
153
                query model.Query) ([]*model.Deployment, int64, error)
154
        SaveDeviceDeploymentLog(ctx context.Context, deviceID string,
155
                deploymentID string, logs []model.LogMessage) error
156
        GetDeviceDeploymentLog(ctx context.Context,
157
                deviceID, deploymentID string) (*model.DeploymentLog, error)
158
        AbortDeviceDeployments(ctx context.Context, deviceID string) error
159
        DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error
160
        DecommissionDevice(ctx context.Context, deviceID string) error
161
        CreateDeviceConfigurationDeployment(
162
                ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
163
                deviceID, deploymentID string) (string, error)
164
        UpdateDeploymentsWithArtifactName(
165
                ctx context.Context,
166
                artifactName string,
167
        ) error
168
}
169

170
type Deployments struct {
171
        db              store.DataStore
172
        objectStorage   storage.ObjectStorage
173
        workflowsClient workflows.Client
174
        inventoryClient inventory.Client
175
        reportingClient reporting.Client
176
}
177

178
func NewDeployments(
179
        storage store.DataStore,
180
        objectStorage storage.ObjectStorage,
181
) *Deployments {
89✔
182
        return &Deployments{
89✔
183
                db:              storage,
89✔
184
                objectStorage:   objectStorage,
89✔
185
                workflowsClient: workflows.NewClient(),
89✔
186
                inventoryClient: inventory.NewClient(),
89✔
187
        }
89✔
188
}
89✔
189

190
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
8✔
191
        d.workflowsClient = workflowsClient
8✔
192
}
8✔
193

194
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
16✔
195
        d.inventoryClient = inventoryClient
16✔
196
}
16✔
197

198
func (d *Deployments) HealthCheck(ctx context.Context) error {
12✔
199
        err := d.db.Ping(ctx)
12✔
200
        if err != nil {
14✔
201
                return errors.Wrap(err, "error reaching MongoDB")
2✔
202
        }
2✔
203
        err = d.objectStorage.HealthCheck(ctx)
10✔
204
        if err != nil {
12✔
205
                return errors.Wrap(
2✔
206
                        err,
2✔
207
                        "error reaching artifact storage service",
2✔
208
                )
2✔
209
        }
2✔
210

211
        err = d.workflowsClient.CheckHealth(ctx)
8✔
212
        if err != nil {
10✔
213
                return errors.Wrap(err, "Workflows service unhealthy")
2✔
214
        }
2✔
215

216
        err = d.inventoryClient.CheckHealth(ctx)
6✔
217
        if err != nil {
8✔
218
                return errors.Wrap(err, "Inventory service unhealthy")
2✔
219
        }
2✔
220

221
        if d.reportingClient != nil {
8✔
222
                err = d.reportingClient.CheckHealth(ctx)
4✔
223
                if err != nil {
6✔
224
                        return errors.Wrap(err, "Reporting service unhealthy")
2✔
225
                }
2✔
226
        }
227
        return nil
2✔
228
}
229

230
func (d *Deployments) contextWithStorageSettings(
231
        ctx context.Context,
232
) (context.Context, error) {
27✔
233
        settings, err := d.db.GetStorageSettings(ctx)
27✔
234
        if err != nil {
27✔
235
                return nil, err
×
236
        } else if settings != nil {
27✔
237
                err = settings.Validate()
×
238
                if err != nil {
×
239
                        return nil, err
×
240
                }
×
241
        }
242
        return storage.SettingsWithContext(ctx, settings), nil
27✔
243
}
244

245
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
6✔
246
        limit, err := d.db.GetLimit(ctx, name)
6✔
247
        if err == mongo.ErrLimitNotFound {
8✔
248
                return &model.Limit{
2✔
249
                        Name:  name,
2✔
250
                        Value: 0,
2✔
251
                }, nil
2✔
252

2✔
253
        } else if err != nil {
8✔
254
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
2✔
255
        }
2✔
256
        return limit, nil
2✔
257
}
258

259
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
5✔
260
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
7✔
261
                return errors.Wrap(err, "failed to provision tenant")
2✔
262
        }
2✔
263

264
        return nil
3✔
265
}
266

267
// CreateImage parses artifact and uploads artifact file to the file storage - in parallel,
268
// and creates image structure in the system.
269
// Returns image ID and nil on success.
270
func (d *Deployments) CreateImage(ctx context.Context,
271
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
1✔
272
        return d.handleArtifact(ctx, multipartUploadMsg)
1✔
273
}
1✔
274

275
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
276
// and creates image structure in the system.
277
// Returns image ID, artifact file ID and nil on success.
278
func (d *Deployments) handleArtifact(ctx context.Context,
279
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
1✔
280

1✔
281
        l := log.FromContext(ctx)
1✔
282
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
283
        if err != nil {
1✔
284
                return "", err
×
285
        }
×
286

287
        // create pipe
288
        pR, pW := io.Pipe()
1✔
289

1✔
290
        tee := io.TeeReader(multipartUploadMsg.ArtifactReader, pW)
1✔
291

1✔
292
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
1✔
293
        if err != nil {
2✔
294
                uid, _ = uuid.NewRandom()
1✔
295
        }
1✔
296
        artifactID := uid.String()
1✔
297

1✔
298
        ch := make(chan error)
1✔
299
        // create goroutine for artifact upload
1✔
300
        //
1✔
301
        // reading from the pipe (which is done in UploadArtifact method) is a blocking operation
1✔
302
        // and cannot be done in the same goroutine as writing to the pipe
1✔
303
        //
1✔
304
        // uploading and parsing artifact in the same process will cause in a deadlock!
1✔
305
        //nolint:errcheck
1✔
306
        go func() (err error) {
2✔
307
                defer func() { ch <- err }()
2✔
308
                err = d.objectStorage.PutObject(
1✔
309
                        ctx, model.ImagePathFromContext(ctx, artifactID), pR,
1✔
310
                )
1✔
311
                if err != nil {
2✔
312
                        pR.CloseWithError(err)
1✔
313
                }
1✔
314
                return err
1✔
315
        }()
316

317
        // parse artifact
318
        // artifact library reads all the data from the given reader
319
        metaArtifactConstructor, err := getMetaFromArchive(&tee)
1✔
320
        if err != nil {
2✔
321
                _ = pW.CloseWithError(err)
1✔
322
                <-ch
1✔
323
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
1✔
324
        }
1✔
325
        // validate artifact metadata
326
        if err = metaArtifactConstructor.Validate(); err != nil {
1✔
327
                return artifactID, ErrModelInvalidMetadata
×
328
        }
×
329

330
        // read the rest of the data,
331
        // just in case the artifact library did not read all the data from the reader
332
        _, err = io.Copy(io.Discard, tee)
1✔
333
        if err != nil {
1✔
334
                // CloseWithError will cause the reading end to abort upload.
×
335
                _ = pW.CloseWithError(err)
×
336
                <-ch
×
337
                return artifactID, err
×
338
        }
×
339

340
        // close the pipe
341
        pW.Close()
1✔
342

1✔
343
        // collect output from the goroutine
1✔
344
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
345
                return artifactID, uploadResponseErr
×
346
        }
×
347

348
        image := model.NewImage(
1✔
349
                artifactID,
1✔
350
                multipartUploadMsg.MetaConstructor,
1✔
351
                metaArtifactConstructor,
1✔
352
                multipartUploadMsg.ArtifactReader.Count(),
1✔
353
        )
1✔
354

1✔
355
        // save image structure in the system
1✔
356
        if err = d.db.InsertImage(ctx, image); err != nil {
1✔
357
                // Try to remove the storage from s3.
×
358
                if errDelete := d.objectStorage.DeleteObject(
×
359
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
360
                ); errDelete != nil {
×
361
                        l.Errorf(
×
362
                                "failed to clean up artifact storage after failure: %s",
×
363
                                errDelete,
×
364
                        )
×
365
                }
×
366
                if idxErr, ok := err.(*model.ConflictError); ok {
×
367
                        return artifactID, idxErr
×
368
                }
×
369
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
370
        }
371
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
372
                return "", errors.Wrap(err, "fail to update deployments")
×
373
        }
×
374

375
        return artifactID, nil
1✔
376
}
377

378
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
379
// creates image structure in the system, and starts the workflow to generate the
380
// artifact from them.
381
// Returns image ID and nil on success.
382
func (d *Deployments) GenerateImage(ctx context.Context,
383
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
21✔
384

21✔
385
        if multipartGenerateImageMsg == nil {
23✔
386
                return "", ErrModelMultipartUploadMsgMalformed
2✔
387
        }
2✔
388

389
        imgID, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
19✔
390
        if err != nil {
25✔
391
                return "", err
6✔
392
        }
6✔
393

394
        multipartGenerateImageMsg.ArtifactID = imgID
13✔
395
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
15✔
396
                multipartGenerateImageMsg.TenantID = id.Tenant
2✔
397
        }
2✔
398

399
        ctx, err = d.contextWithStorageSettings(ctx)
13✔
400
        if err != nil {
13✔
401
                return "", err
×
402
        }
×
403
        imgPath := model.ImagePathFromContext(ctx, imgID)
13✔
404

13✔
405
        link, err := d.objectStorage.GetRequest(
13✔
406
                ctx,
13✔
407
                imgPath,
13✔
408
                imgID+model.ArtifactFileSuffix,
13✔
409
                DefaultImageGenerationLinkExpire,
13✔
410
        )
13✔
411
        if err != nil {
15✔
412
                return "", err
2✔
413
        }
2✔
414
        multipartGenerateImageMsg.GetArtifactURI = link.Uri
11✔
415

11✔
416
        link, err = d.objectStorage.DeleteRequest(ctx, imgPath, DefaultImageGenerationLinkExpire)
11✔
417
        if err != nil {
13✔
418
                return "", err
2✔
419
        }
2✔
420
        multipartGenerateImageMsg.DeleteArtifactURI = link.Uri
9✔
421

9✔
422
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
9✔
423
        if err != nil {
13✔
424
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
6✔
425
                        return "", errors.Wrap(err, cleanupErr.Error())
2✔
426
                }
2✔
427
                return "", err
2✔
428
        }
429

430
        return imgID, err
5✔
431
}
432

433
func (d *Deployments) GenerateConfigurationImage(
434
        ctx context.Context,
435
        deviceType string,
436
        deploymentID string,
437
) (io.Reader, error) {
9✔
438
        var buf bytes.Buffer
9✔
439
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
9✔
440
        if err != nil {
11✔
441
                return nil, err
2✔
442
        } else if dpl == nil {
11✔
443
                return nil, ErrModelDeploymentNotFound
2✔
444
        }
2✔
445
        var metaData map[string]interface{}
5✔
446
        err = json.Unmarshal(dpl.Configuration, &metaData)
5✔
447
        if err != nil {
7✔
448
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
2✔
449
        }
2✔
450

451
        artieWriter := awriter.NewWriter(&buf, artifact.NewCompressorNone())
3✔
452
        module := handlers.NewModuleImage(ArtifactConfigureType)
3✔
453
        err = artieWriter.WriteArtifact(&awriter.WriteArtifactArgs{
3✔
454
                Format:  "mender",
3✔
455
                Version: 3,
3✔
456
                Devices: []string{deviceType},
3✔
457
                Name:    dpl.ArtifactName,
3✔
458
                Updates: &awriter.Updates{Updates: []handlers.Composer{module}},
3✔
459
                Depends: &artifact.ArtifactDepends{
3✔
460
                        CompatibleDevices: []string{deviceType},
3✔
461
                },
3✔
462
                Provides: &artifact.ArtifactProvides{
3✔
463
                        ArtifactName: dpl.ArtifactName,
3✔
464
                },
3✔
465
                MetaData: metaData,
3✔
466
                TypeInfoV3: &artifact.TypeInfoV3{
3✔
467
                        Type: &ArtifactConfigureType,
3✔
468
                        ArtifactProvides: artifact.TypeInfoProvides{
3✔
469
                                ArtifactConfigureProvides: dpl.ArtifactName,
3✔
470
                        },
3✔
471
                        ArtifactDepends:        artifact.TypeInfoDepends{},
3✔
472
                        ClearsArtifactProvides: []string{ArtifactConfigureProvidesCleared},
3✔
473
                },
3✔
474
        })
3✔
475

3✔
476
        return &buf, err
3✔
477
}
478

479
// handleRawFile parses raw data, uploads it to the file storage,
480
// and starts the workflow to generate the artifact.
481
// Returns image ID, artifact file ID and nil on success.
482
func (d *Deployments) handleRawFile(ctx context.Context,
483
        multipartMsg *model.MultipartGenerateImageMsg) (string, error) {
19✔
484

19✔
485
        uid, _ := uuid.NewRandom()
19✔
486
        artifactID := uid.String()
19✔
487

19✔
488
        // check if artifact is unique
19✔
489
        // artifact is considered to be unique if there is no artifact with the same name
19✔
490
        // and supporting the same platform in the system
19✔
491
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
19✔
492
                multipartMsg.Name,
19✔
493
                multipartMsg.DeviceTypesCompatible,
19✔
494
        )
19✔
495
        if err != nil {
21✔
496
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
2✔
497
        }
2✔
498
        if !isArtifactUnique {
19✔
499
                return "", ErrModelArtifactNotUnique
2✔
500
        }
2✔
501

502
        ctx, err = d.contextWithStorageSettings(ctx)
15✔
503
        if err != nil {
15✔
504
                return "", err
×
505
        }
×
506
        err = d.objectStorage.PutObject(
15✔
507
                ctx, model.ImagePathFromContext(ctx, artifactID), multipartMsg.FileReader,
15✔
508
        )
15✔
509
        if err != nil {
17✔
510
                return "", err
2✔
511
        }
2✔
512

513
        return artifactID, nil
13✔
514
}
515

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

1✔
520
        image, err := d.db.FindImageByID(ctx, id)
1✔
521
        if err != nil {
1✔
522
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
523
        }
×
524

525
        if image == nil {
2✔
526
                return nil, nil
1✔
527
        }
1✔
528

529
        return image, nil
1✔
530
}
531

532
// DeleteImage removes metadata and image file
533
// Noop for not existing images
534
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
535
// file is needed
536
// In case of already finished updates only image file is not needed, metadata is attached directly
537
// to device deployment therefore we still have some information about image that have been used
538
// (but not the file)
539
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
1✔
540
        found, err := d.GetImage(ctx, imageID)
1✔
541

1✔
542
        if err != nil {
1✔
543
                return errors.Wrap(err, "Getting image metadata")
×
544
        }
×
545

546
        if found == nil {
1✔
547
                return ErrImageMetaNotFound
×
548
        }
×
549

550
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
551
        if err != nil {
1✔
552
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
553
        }
×
554

555
        // Image is in use, not allowed to delete
556
        if inUse {
2✔
557
                return ErrModelImageInActiveDeployment
1✔
558
        }
1✔
559

560
        // Delete image file (call to external service)
561
        // Noop for not existing file
562
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
563
        if err != nil {
1✔
564
                return err
×
565
        }
×
566
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
567
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
568
                return errors.Wrap(err, "Deleting image file")
×
569
        }
×
570

571
        // Delete metadata
572
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
573
                return errors.Wrap(err, "Deleting image metadata")
×
574
        }
×
575

576
        return nil
1✔
577
}
578

579
// ListImages according to specified filers.
580
func (d *Deployments) ListImages(
581
        ctx context.Context,
582
        filters *model.ReleaseOrImageFilter,
583
) ([]*model.Image, int, error) {
1✔
584
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
585
        if err != nil {
1✔
586
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
587
        }
×
588

589
        if imageList == nil {
2✔
590
                return make([]*model.Image, 0), 0, nil
1✔
591
        }
1✔
592

593
        return imageList, count, nil
1✔
594
}
595

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

×
600
        if err := constructor.Validate(); err != nil {
×
601
                return false, errors.Wrap(err, "Validating image metadata")
×
602
        }
×
603

604
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
605
        if err != nil {
×
606
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
607
        }
×
608

609
        if found {
×
610
                return false, ErrModelImageUsedInAnyDeployment
×
611
        }
×
612

613
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
614
        if err != nil {
×
615
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
616
        }
×
617

618
        if foundImage == nil {
×
619
                return false, nil
×
620
        }
×
621

622
        foundImage.SetModified(time.Now())
×
623
        foundImage.ImageMeta = constructor
×
624

×
625
        _, err = d.db.Update(ctx, foundImage)
×
626
        if err != nil {
×
627
                return false, errors.Wrap(err, "Updating image matadata")
×
628
        }
×
629

630
        return true, nil
×
631
}
632

633
// DownloadLink presigned GET link to download image file.
634
// Returns error if image have not been uploaded.
635
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
636
        expire time.Duration) (*model.Link, error) {
1✔
637

1✔
638
        image, err := d.GetImage(ctx, imageID)
1✔
639
        if err != nil {
1✔
640
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
641
        }
×
642

643
        if image == nil {
1✔
644
                return nil, nil
×
645
        }
×
646

647
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
648
        if err != nil {
1✔
649
                return nil, err
×
650
        }
×
651
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
652
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
653
        if err != nil {
1✔
654
                return nil, errors.Wrap(err, "Searching for image file")
×
655
        }
×
656

657
        link, err := d.objectStorage.GetRequest(
1✔
658
                ctx,
1✔
659
                imagePath,
1✔
660
                image.Name+model.ArtifactFileSuffix,
1✔
661
                expire,
1✔
662
        )
1✔
663
        if err != nil {
1✔
664
                return nil, errors.Wrap(err, "Generating download link")
×
665
        }
×
666

667
        return link, nil
1✔
668
}
669

670
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
671
        return &model.ArtifactInfo{
1✔
672
                Format:  info.Format,
1✔
673
                Version: uint(info.Version),
1✔
674
        }
1✔
675
}
1✔
676

677
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
678
        var files []model.UpdateFile
1✔
679
        for _, u := range uFiles {
2✔
680
                files = append(files, model.UpdateFile{
1✔
681
                        Name:     u.Name,
1✔
682
                        Size:     u.Size,
1✔
683
                        Date:     &u.Date,
1✔
684
                        Checksum: string(u.Checksum),
1✔
685
                })
1✔
686
        }
1✔
687
        return files, nil
1✔
688
}
689

690
func getMetaFromArchive(r *io.Reader) (*model.ArtifactMeta, error) {
1✔
691
        metaArtifact := model.NewArtifactMeta()
1✔
692

1✔
693
        aReader := areader.NewReader(*r)
1✔
694

1✔
695
        // There is no signature verification here.
1✔
696
        // It is just simple check if artifact is signed or not.
1✔
697
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
1✔
698
                metaArtifact.Signed = true
×
699
                return nil
×
700
        }
×
701

702
        err := aReader.ReadArtifact()
1✔
703
        if err != nil {
2✔
704
                return nil, errors.Wrap(err, "reading artifact error")
1✔
705
        }
1✔
706

707
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
708
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
709

1✔
710
        metaArtifact.Name = aReader.GetArtifactName()
1✔
711
        if metaArtifact.Info.Version == 3 {
2✔
712
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
713
                if err != nil {
1✔
714
                        return nil, errors.Wrap(err,
×
715
                                "error parsing version 3 artifact")
×
716
                }
×
717

718
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
719
                if err != nil {
1✔
720
                        return nil, errors.Wrap(err,
×
721
                                "error parsing version 3 artifact")
×
722
                }
×
723

724
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
725
        }
726

727
        for _, p := range aReader.GetHandlers() {
2✔
728
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
729
                if err != nil {
1✔
730
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
731
                }
×
732

733
                uMetadata, err := p.GetUpdateMetaData()
1✔
734
                if err != nil {
1✔
735
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
736
                }
×
737

738
                metaArtifact.Updates = append(
1✔
739
                        metaArtifact.Updates,
1✔
740
                        model.Update{
1✔
741
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
742
                                        Type: p.GetUpdateType(),
1✔
743
                                },
1✔
744
                                Files:    uFiles,
1✔
745
                                MetaData: uMetadata,
1✔
746
                        })
1✔
747
        }
748

749
        return metaArtifact, nil
1✔
750
}
751

752
func getArtifactIDs(artifacts []*model.Image) []string {
13✔
753
        artifactIDs := make([]string, 0, len(artifacts))
13✔
754
        for _, artifact := range artifacts {
26✔
755
                artifactIDs = append(artifactIDs, artifact.Id)
13✔
756
        }
13✔
757
        return artifactIDs
13✔
758
}
759

760
// deployments
761
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
8✔
762
        ids := make([]string, len(devices))
8✔
763
        for i, d := range devices {
16✔
764
                ids[i] = d.ID
8✔
765
        }
8✔
766

767
        return ids
8✔
768
}
769

770
// updateDeploymentConstructor fills devices list with device ids
771
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
772
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
10✔
773
        l := log.FromContext(ctx)
10✔
774

10✔
775
        id := identity.FromContext(ctx)
10✔
776
        if id == nil {
10✔
777
                l.Error("identity not present in the context")
×
778
                return nil, ErrModelInternal
×
779
        }
×
780
        searchParams := model.SearchParams{
10✔
781
                Page:    1,
10✔
782
                PerPage: PerPageInventoryDevices,
10✔
783
                Filters: []model.FilterPredicate{
10✔
784
                        {
10✔
785
                                Scope:     InventoryIdentityScope,
10✔
786
                                Attribute: InventoryStatusAttributeName,
10✔
787
                                Type:      "$eq",
10✔
788
                                Value:     InventoryStatusAccepted,
10✔
789
                        },
10✔
790
                },
10✔
791
        }
10✔
792
        if len(constructor.Group) > 0 {
20✔
793
                searchParams.Filters = append(
10✔
794
                        searchParams.Filters,
10✔
795
                        model.FilterPredicate{
10✔
796
                                Scope:     InventoryGroupScope,
10✔
797
                                Attribute: InventoryGroupAttributeName,
10✔
798
                                Type:      "$eq",
10✔
799
                                Value:     constructor.Group,
10✔
800
                        })
10✔
801
        }
10✔
802

803
        for {
22✔
804
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
12✔
805
                if err != nil {
14✔
806
                        l.Errorf("error searching for devices")
2✔
807
                        return nil, ErrModelInternal
2✔
808
                }
2✔
809
                if count < 1 {
12✔
810
                        l.Errorf("no devices found")
2✔
811
                        return nil, ErrNoDevices
2✔
812
                }
2✔
813
                if len(devices) < 1 {
8✔
814
                        break
×
815
                }
816
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
8✔
817
                if len(constructor.Devices) == count {
14✔
818
                        break
6✔
819
                }
820
                searchParams.Page++
2✔
821
        }
822

823
        return constructor, nil
6✔
824
}
825

826
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
827
func (d *Deployments) CreateDeviceConfigurationDeployment(
828
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
829
        deviceID, deploymentID string) (string, error) {
9✔
830

9✔
831
        if constructor == nil {
11✔
832
                return "", ErrModelMissingInput
2✔
833
        }
2✔
834

835
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
7✔
836
                constructor,
7✔
837
                deploymentID,
7✔
838
        )
7✔
839
        if err != nil {
7✔
840
                return "", errors.Wrap(err, "failed to create deployment")
×
841
        }
×
842

843
        deployment.DeviceList = []string{deviceID}
7✔
844
        deployment.MaxDevices = 1
7✔
845
        deployment.Configuration = []byte(constructor.Configuration)
7✔
846
        deployment.Type = model.DeploymentTypeConfiguration
7✔
847

7✔
848
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
7✔
849
        if err != nil {
9✔
850
                return "", err
2✔
851
        }
2✔
852
        deployment.Groups = groups
5✔
853

5✔
854
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
8✔
855
                if strings.Contains(err.Error(), "duplicate key error") {
4✔
856
                        return "", ErrDuplicateDeployment
1✔
857
                }
1✔
858
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
4✔
859
                        return "", ErrInvalidDeploymentID
1✔
860
                }
1✔
861
                return "", errors.Wrap(err, "Storing deployment data")
2✔
862
        }
863

864
        return deployment.Id, nil
3✔
865
}
866

867
// CreateDeployment precomputes new deployment and schedules it for devices.
868
func (d *Deployments) CreateDeployment(ctx context.Context,
869
        constructor *model.DeploymentConstructor) (string, error) {
17✔
870

17✔
871
        var err error
17✔
872

17✔
873
        if constructor == nil {
19✔
874
                return "", ErrModelMissingInput
2✔
875
        }
2✔
876

877
        if err := constructor.Validate(); err != nil {
15✔
878
                return "", errors.Wrap(err, "Validating deployment")
×
879
        }
×
880

881
        if len(constructor.Group) > 0 || constructor.AllDevices {
25✔
882
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
10✔
883
                if err != nil {
14✔
884
                        return "", err
4✔
885
                }
4✔
886
        }
887

888
        deployment, err := model.NewDeploymentFromConstructor(constructor)
11✔
889
        if err != nil {
11✔
890
                return "", errors.Wrap(err, "failed to create deployment")
×
891
        }
×
892

893
        // Assign artifacts to the deployment.
894
        // When new artifact(s) with the artifact name same as the one in the deployment
895
        // will be uploaded to the backend, it will also become part of this deployment.
896
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
11✔
897
        if err != nil {
11✔
898
                return "", errors.Wrap(err, "Finding artifact with given name")
×
899
        }
×
900

901
        if len(artifacts) == 0 {
12✔
902
                return "", ErrNoArtifact
1✔
903
        }
1✔
904

905
        deployment.Artifacts = getArtifactIDs(artifacts)
11✔
906
        deployment.DeviceList = constructor.Devices
11✔
907
        deployment.MaxDevices = len(constructor.Devices)
11✔
908
        deployment.Type = model.DeploymentTypeSoftware
11✔
909
        if len(constructor.Group) > 0 {
17✔
910
                deployment.Groups = []string{constructor.Group}
6✔
911
        }
6✔
912

913
        // single device deployment case
914
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
16✔
915
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
5✔
916
                if err != nil {
5✔
917
                        return "", err
×
918
                }
×
919
                deployment.Groups = groups
5✔
920
        }
921

922
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
13✔
923
                return "", errors.Wrap(err, "Storing deployment data")
2✔
924
        }
2✔
925

926
        return deployment.Id, nil
9✔
927
}
928

929
func (d *Deployments) getDeploymentGroups(
930
        ctx context.Context,
931
        devices []string,
932
) ([]string, error) {
11✔
933
        id := identity.FromContext(ctx)
11✔
934

11✔
935
        //only for single device deployment case
11✔
936
        if len(devices) != 1 {
11✔
937
                return nil, nil
×
938
        }
×
939

940
        if id == nil {
12✔
941
                id = &identity.Identity{}
1✔
942
        }
1✔
943

944
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
11✔
945
        if err != nil && err != inventory.ErrDevNotFound {
13✔
946
                return nil, err
2✔
947
        }
2✔
948
        return groups, nil
9✔
949
}
950

951
// IsDeploymentFinished checks if there is unfinished deployment with given ID
952
func (d *Deployments) IsDeploymentFinished(
953
        ctx context.Context,
954
        deploymentID string,
955
) (bool, error) {
1✔
956
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
957
        if err != nil {
1✔
958
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
959
        }
×
960
        if deployment == nil {
2✔
961
                return true, nil
1✔
962
        }
1✔
963

964
        return false, nil
1✔
965
}
966

967
// GetDeployment fetches deployment by ID
968
func (d *Deployments) GetDeployment(ctx context.Context,
969
        deploymentID string) (*model.Deployment, error) {
1✔
970

1✔
971
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
972
        if err != nil {
1✔
973
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
974
        }
×
975

976
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
977
                return nil, err
×
978
        }
×
979

980
        return deployment, nil
1✔
981
}
982

983
// ImageUsedInActiveDeployment checks if specified image is in use by deployments Image is
984
// considered to be in use if it's participating in at lest one non success/error deployment.
985
func (d *Deployments) ImageUsedInActiveDeployment(ctx context.Context,
986
        imageID string) (bool, error) {
7✔
987

7✔
988
        var found bool
7✔
989

7✔
990
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
7✔
991
        if err != nil {
9✔
992
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
2✔
993
        }
2✔
994

995
        if found {
6✔
996
                return found, nil
1✔
997
        }
1✔
998

999
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
5✔
1000
                imageID, model.ActiveDeploymentStatuses()...)
5✔
1001
        if err != nil {
7✔
1002
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
2✔
1003
        }
2✔
1004

1005
        return found, nil
3✔
1006
}
1007

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

×
1012
        var found bool
×
1013

×
1014
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1015
        if err != nil {
×
1016
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1017
        }
×
1018

1019
        if found {
×
1020
                return found, nil
×
1021
        }
×
1022

1023
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
1024
        if err != nil {
×
1025
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
1026
        }
×
1027

1028
        return found, nil
×
1029
}
1030

1031
// assignArtifact assigns artifact to the device deployment
1032
func (d *Deployments) assignArtifact(
1033
        ctx context.Context,
1034
        deployment *model.Deployment,
1035
        deviceDeployment *model.DeviceDeployment,
1036
        installed *model.InstalledDeviceDeployment) error {
1✔
1037

1✔
1038
        // Assign artifact to the device deployment.
1✔
1039
        var artifact *model.Image
1✔
1040
        var err error
1✔
1041

1✔
1042
        if err = installed.Validate(); err != nil {
1✔
1043
                return err
×
1044
        }
×
1045

1046
        if deviceDeployment.DeploymentId == "" || deviceDeployment.DeviceId == "" {
1✔
1047
                return ErrModelInternal
×
1048
        }
×
1049

1050
        // Clear device deployment image
1051
        // New artifact will be selected for the device deployment
1052
        deviceDeployment.Image = nil
1✔
1053

1✔
1054
        // First case is for backward compatibility.
1✔
1055
        // It is possible that there is old deployment structure in the system.
1✔
1056
        // In such case we need to select artifact using name and device type.
1✔
1057
        if deployment.Artifacts == nil || len(deployment.Artifacts) == 0 {
1✔
1058
                artifact, err = d.db.ImageByNameAndDeviceType(
×
1059
                        ctx,
×
1060
                        installed.ArtifactName,
×
1061
                        installed.DeviceType,
×
1062
                )
×
1063
                if err != nil {
×
1064
                        return errors.Wrap(err, "assigning artifact to device deployment")
×
1065
                }
×
1066
        } else {
1✔
1067
                // Select artifact for the device deployment from artifacts assigned to the deployment.
1✔
1068
                artifact, err = d.db.ImageByIdsAndDeviceType(
1✔
1069
                        ctx,
1✔
1070
                        deployment.Artifacts,
1✔
1071
                        installed.DeviceType,
1✔
1072
                )
1✔
1073
                if err != nil {
1✔
1074
                        return errors.Wrap(err, "assigning artifact to device deployment")
×
1075
                }
×
1076
        }
1077

1078
        // If not having appropriate image, set noartifact status
1079
        if artifact == nil {
1✔
1080
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
×
1081
                        deviceDeployment.DeviceId,
×
1082
                        model.DeviceDeploymentState{
×
1083
                                Status: model.DeviceDeploymentStatusNoArtifact,
×
1084
                        }); err != nil {
×
1085
                        return errors.Wrap(err, "Failed to update deployment status")
×
1086
                }
×
NEW
1087
                if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
×
NEW
1088
                        l := log.FromContext(ctx)
×
NEW
1089
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1090
                }
×
UNCOV
1091
                return nil
×
1092
        }
1093

1094
        if err := d.db.AssignArtifact(
1✔
1095
                ctx,
1✔
1096
                deviceDeployment.DeviceId,
1✔
1097
                deviceDeployment.DeploymentId,
1✔
1098
                artifact,
1✔
1099
        ); err != nil {
1✔
1100
                return errors.Wrap(err, "Assigning artifact to the device deployment")
×
1101
        }
×
1102

1103
        deviceDeployment.Image = artifact
1✔
1104

1✔
1105
        return nil
1✔
1106
}
1107

1108
// Retrieves the model.Deployment and model.DeviceDeployment structures
1109
// for the device. Upon error, nil is returned for both deployment structures.
1110
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1111
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
3✔
1112

3✔
1113
        // Retrieve device deployment
3✔
1114
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
3✔
1115

3✔
1116
        if err != nil {
3✔
1117
                return nil, nil, errors.Wrap(err,
×
1118
                        "Searching for oldest active deployment for the device")
×
1119
        } else if deviceDeployment == nil {
4✔
1120
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1121
        }
1✔
1122

1123
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
3✔
1124
        if err != nil {
3✔
1125
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1126
        }
×
1127
        if deployment == nil {
3✔
1128
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1129
        }
×
1130

1131
        return deployment, deviceDeployment, nil
3✔
1132
}
1133

1134
// getNewDeploymentForDevice returns deployment object and creates and returns
1135
// new device deployment for the device;
1136
//
1137
// we are interested only in the deployments that are newer than the latest
1138
// deployment applied by the device;
1139
// this way we guarantee that the device will not receive deployment
1140
// that is older than the one installed on the device;
1141
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1142
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1143

1✔
1144
        var lastDeployment *time.Time
1✔
1145
        //get latest device deployment for the device;
1✔
1146
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1147
        if err != nil {
1✔
1148
                return nil, nil, errors.Wrap(err,
×
1149
                        "Searching for latest active deployment for the device")
×
1150
        } else if deviceDeployment == nil {
2✔
1151
                lastDeployment = &time.Time{}
1✔
1152
        } else {
2✔
1153
                lastDeployment = deviceDeployment.Created
1✔
1154
        }
1✔
1155

1156
        //get deployments newer then last device deployment
1157
        //iterate over deployments and check if the device is part of the deployment or not
1158
        for skip := 0; true; skip += 100 {
2✔
1159
                deployments, err := d.db.FindNewerActiveDeployments(ctx, lastDeployment, skip, 100)
1✔
1160
                if err != nil {
1✔
1161
                        return nil, nil, errors.Wrap(err,
×
1162
                                "Failed to search for newer active deployments")
×
1163
                }
×
1164
                if len(deployments) == 0 {
2✔
1165
                        return nil, nil, nil
1✔
1166
                }
1✔
1167

1168
                for _, deployment := range deployments {
2✔
1169
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1170
                        if err != nil {
1✔
1171
                                return nil, nil, err
×
1172
                        }
×
1173
                        if ok {
2✔
1174
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1175
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1176
                                if err != nil {
1✔
1177
                                        return nil, nil, err
×
1178
                                }
×
1179
                                return deployment, deviceDeployment, nil
1✔
1180
                        }
1181
                }
1182
        }
1183

1184
        return nil, nil, nil
×
1185
}
1186

1187
func (d *Deployments) createDeviceDeploymentWithStatus(
1188
        ctx context.Context, deviceID string,
1189
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1190
) (*model.DeviceDeployment, error) {
11✔
1191
        prevStatus := model.DeviceDeploymentStatusNull
11✔
1192
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
11✔
1193
        if err != nil && err != mongo.ErrStorageNotFound {
11✔
1194
                return nil, err
×
1195
        } else if deviceDeployment != nil {
11✔
1196
                prevStatus = deviceDeployment.Status
×
1197
        }
×
1198

1199
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
11✔
1200
        deviceDeployment.Status = status
11✔
1201
        deviceDeployment.Active = status.Active()
11✔
1202
        deviceDeployment.Created = deployment.Created
11✔
1203

11✔
1204
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
11✔
1205
                return nil, err
×
1206
        }
×
1207

1208
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
11✔
1209
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
11✔
1210
                return nil, err
×
1211
        }
×
1212

1213
        // after inserting new device deployment update deployment stats
1214
        // in the database and locally, and update deployment status
1215
        if err := d.db.UpdateStatsInc(
11✔
1216
                ctx, deployment.Id,
11✔
1217
                prevStatus, status,
11✔
1218
        ); err != nil {
11✔
1219
                return nil, err
×
1220
        }
×
1221

1222
        deployment.Stats.Inc(status)
11✔
1223

11✔
1224
        err = d.recalcDeploymentStatus(ctx, deployment)
11✔
1225
        if err != nil {
11✔
1226
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1227
        }
×
1228

1229
        if !status.Active() {
21✔
1230
                err := d.reindexDevice(ctx, deviceID)
10✔
1231
                if err != nil {
10✔
NEW
1232
                        l := log.FromContext(ctx)
×
NEW
1233
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1234
                }
×
1235
        }
1236

1237
        return deviceDeployment, nil
11✔
1238
}
1239

1240
func (d *Deployments) isDevicePartOfDeployment(
1241
        ctx context.Context,
1242
        deviceID string,
1243
        deployment *model.Deployment,
1244
) (bool, error) {
15✔
1245
        for _, id := range deployment.DeviceList {
26✔
1246
                if id == deviceID {
22✔
1247
                        return true, nil
11✔
1248
                }
11✔
1249
        }
1250
        return false, nil
5✔
1251
}
1252

1253
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1254
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1255
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
3✔
1256

3✔
1257
        l := log.FromContext(ctx)
3✔
1258

3✔
1259
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
3✔
1260
        if err != nil {
3✔
1261
                return nil, ErrModelInternal
×
1262
        } else if deployment == nil {
4✔
1263
                return nil, nil
1✔
1264
        }
1✔
1265

1266
        if deviceDeployment.Request != nil {
4✔
1267
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1268
                        // the device reported different device type and/or artifact name
1✔
1269
                        // during the update process, which should never happen;
1✔
1270
                        // mark deployment for this device as failed to force client to rollback
1✔
1271
                        l.Errorf(
1✔
1272
                                "Device with id %s reported new data: %s during update process;"+
1✔
1273
                                        "old data: %s",
1✔
1274
                                deviceID, request, deviceDeployment.Request)
1✔
1275

1✔
1276
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1277
                                model.DeviceDeploymentState{
1✔
1278
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1279
                                }); err != nil {
1✔
1280
                                return nil, errors.Wrap(err, "Failed to update deployment status")
×
1281
                        }
×
1282
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
NEW
1283
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1284
                        }
×
1285
                        return nil, ErrConflictingRequestData
1✔
1286
                }
1287
        } else {
3✔
1288
                // save the request
3✔
1289
                if err := d.db.SaveDeviceDeploymentRequest(
3✔
1290
                        ctx, deviceDeployment.Id, request); err != nil {
3✔
1291
                        return nil, err
×
1292
                }
×
1293
        }
1294

1295
        // if the deployment is not forcing the installation, and if the device
1296
        // reported same artifact name as the one in the device deployment, and this is
1297
        // a new device deployment - indicated by device deployment status "pending",
1298
        // pretend there is no deployment for this device, but update
1299
        // its status to already installed first
1300
        if !deployment.ForceInstallation &&
3✔
1301
                request.DeviceProvides.ArtifactName != "" &&
3✔
1302
                deployment.ArtifactName == request.DeviceProvides.ArtifactName &&
3✔
1303
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
6✔
1304

3✔
1305
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
3✔
1306
                        model.DeviceDeploymentState{
3✔
1307
                                Status: model.DeviceDeploymentStatusAlreadyInst,
3✔
1308
                        }); err != nil {
3✔
NEW
1309
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
×
NEW
1310
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1311
                        }
×
1312
                        return nil, errors.Wrap(err, "Failed to update deployment status")
×
1313
                }
1314

1315
                return nil, nil
3✔
1316
        }
1317

1318
        if deployment.Type == model.DeploymentTypeConfiguration {
2✔
1319
                // There's nothing more we need to do, the link must be filled
1✔
1320
                // in by the API layer.
1✔
1321
                return &model.DeploymentInstructions{
1✔
1322
                        ID: deployment.Id,
1✔
1323
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1324
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1325
                                // use deployment ID togheter with device ID as artifact ID
1✔
1326
                                ID:                    deployment.Id + deviceID,
1✔
1327
                                ArtifactName:          deployment.ArtifactName,
1✔
1328
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1329
                        },
1✔
1330
                        Type: model.DeploymentTypeConfiguration,
1✔
1331
                }, nil
1✔
1332
        }
1✔
1333

1334
        // assign artifact only if the artifact was not assigned previously
1335
        if deviceDeployment.Image == nil {
2✔
1336
                if err := d.assignArtifact(
1✔
1337
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
1✔
1338
                        return nil, err
×
1339
                }
×
1340
        }
1341

1342
        if deviceDeployment.Image == nil {
1✔
1343
                return nil, nil
×
1344
        }
×
1345

1346
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
1347
        if err != nil {
1✔
1348
                return nil, err
×
1349
        }
×
1350

1351
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1352
        link, err := d.objectStorage.GetRequest(
1✔
1353
                ctx,
1✔
1354
                imagePath,
1✔
1355
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1356
                DefaultUpdateDownloadLinkExpire,
1✔
1357
        )
1✔
1358
        if err != nil {
1✔
1359
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1360
        }
×
1361

1362
        instructions := &model.DeploymentInstructions{
1✔
1363
                ID: deviceDeployment.DeploymentId,
1✔
1364
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1365
                        ID: deviceDeployment.Image.Id,
1✔
1366
                        ArtifactName: deviceDeployment.Image.
1✔
1367
                                ArtifactMeta.Name,
1✔
1368
                        Source: *link,
1✔
1369
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1370
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1371
                },
1✔
1372
        }
1✔
1373

1✔
1374
        return instructions, nil
1✔
1375
}
1376

1377
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1378
// ID `deviceID`. Returns nil if update was successful.
1379
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1380
        deviceID string, ddState model.DeviceDeploymentState) error {
11✔
1381

11✔
1382
        l := log.FromContext(ctx)
11✔
1383

11✔
1384
        l.Infof("New status: %s for device %s deployment: %v", ddState.Status, deviceID, deploymentID)
11✔
1385

11✔
1386
        var finishTime *time.Time = nil
11✔
1387
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
18✔
1388
                now := time.Now()
7✔
1389
                finishTime = &now
7✔
1390
        }
7✔
1391

1392
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
11✔
1393
        if err == mongo.ErrStorageNotFound {
13✔
1394
                return ErrStorageNotFound
2✔
1395
        } else if err != nil {
11✔
1396
                return err
×
1397
        }
×
1398

1399
        currentStatus := dd.Status
9✔
1400

9✔
1401
        if currentStatus == model.DeviceDeploymentStatusAborted {
9✔
1402
                return ErrDeploymentAborted
×
1403
        }
×
1404

1405
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
9✔
1406
                return ErrDeviceDecommissioned
×
1407
        }
×
1408

1409
        // nothing to do
1410
        if ddState.Status == currentStatus {
9✔
1411
                return nil
×
1412
        }
×
1413

1414
        // update finish time
1415
        ddState.FinishTime = finishTime
9✔
1416

9✔
1417
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
9✔
1418
                deviceID, deploymentID, ddState)
9✔
1419
        if err != nil {
9✔
1420
                return err
×
1421
        }
×
1422

1423
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
9✔
1424
                return err
×
1425
        }
×
1426

1427
        // fetch deployment stats and update deployment status
1428
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
9✔
1429
        if err != nil {
9✔
1430
                return errors.Wrap(err, "failed when searching for deployment")
×
1431
        }
×
1432

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

1438
        if !ddState.Status.Active() {
16✔
1439
                if err := d.reindexDevice(ctx, deviceID); err != nil {
7✔
NEW
1440
                        l := log.FromContext(ctx)
×
NEW
1441
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1442
                }
×
1443
        }
1444

1445
        return nil
9✔
1446
}
1447

1448
// recalcDeploymentStatus inspects the deployment stats and
1449
// recalculates and updates its status
1450
// it should be used whenever deployment stats are touched
1451
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
19✔
1452
        status := dep.GetStatus()
19✔
1453

19✔
1454
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
19✔
1455
                return err
×
1456
        }
×
1457

1458
        return nil
19✔
1459
}
1460

1461
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1462
        deploymentID string) (model.Stats, error) {
1✔
1463

1✔
1464
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1465

1✔
1466
        if err != nil {
1✔
1467
                return nil, errors.Wrap(err, "checking deployment id")
×
1468
        }
×
1469

1470
        if deployment == nil {
1✔
1471
                return nil, nil
×
1472
        }
×
1473

1474
        return deployment.Stats, nil
1✔
1475
}
1476
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1477
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1478

×
1479
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1480

×
1481
        if err != nil {
×
1482
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1483
        }
×
1484

1485
        if deploymentStats == nil {
×
1486
                return nil, ErrModelDeploymentNotFound
×
1487
        }
×
1488

1489
        return deploymentStats, nil
×
1490
}
1491

1492
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1493
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1494
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1495

1✔
1496
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1497
        if err != nil {
1✔
1498
                return nil, ErrModelInternal
×
1499
        }
×
1500

1501
        if deployment == nil {
1✔
1502
                return nil, ErrModelDeploymentNotFound
×
1503
        }
×
1504

1505
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1506
        if err != nil {
1✔
1507
                return nil, ErrModelInternal
×
1508
        }
×
1509

1510
        return statuses, nil
1✔
1511
}
1512

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

1✔
1516
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1517
        if err != nil {
1✔
1518
                return nil, -1, ErrModelInternal
×
1519
        }
×
1520

1521
        if deployment == nil {
1✔
1522
                return nil, -1, ErrModelDeploymentNotFound
×
1523
        }
×
1524

1525
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1526
        if err != nil {
1✔
1527
                return nil, -1, ErrModelInternal
×
1528
        }
×
1529

1530
        return statuses, totalCount, nil
1✔
1531
}
1532

1533
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1534
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
8✔
1535
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
8✔
1536
        if err != nil {
10✔
1537
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
2✔
1538
        }
2✔
1539

1540
        deploymentIDs := make([]string, len(deviceDeployments))
6✔
1541
        for i, deviceDeployment := range deviceDeployments {
18✔
1542
                deploymentIDs[i] = deviceDeployment.DeploymentId
12✔
1543
        }
12✔
1544

1545
        deployments, _, err := d.db.Find(ctx, model.Query{
6✔
1546
                IDs:          deploymentIDs,
6✔
1547
                Limit:        len(deviceDeployments),
6✔
1548
                DisableCount: true,
6✔
1549
        })
6✔
1550
        if err != nil {
8✔
1551
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
2✔
1552
        }
2✔
1553

1554
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
4✔
1555
        for _, deployment := range deployments {
10✔
1556
                deploymentsMap[deployment.Id] = deployment
6✔
1557
        }
6✔
1558

1559
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
4✔
1560
        for _, deviceDeployment := range deviceDeployments {
12✔
1561
                device := model.DeviceDeploymentWithImage(deviceDeployment)
8✔
1562
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
14✔
1563
                        res = append(res, model.DeviceDeploymentListItem{
6✔
1564
                                Id:         deviceDeployment.Id,
6✔
1565
                                Deployment: deployment,
6✔
1566
                                Device:     &device,
6✔
1567
                        })
6✔
1568
                } else {
8✔
1569
                        res = append(res, model.DeviceDeploymentListItem{
2✔
1570
                                Id:     deviceDeployment.Id,
2✔
1571
                                Device: &device,
2✔
1572
                        })
2✔
1573

2✔
1574
                }
2✔
1575
        }
1576

1577
        return res, totalCount, nil
4✔
1578
}
1579

1580
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1581
        ctx context.Context,
1582
        deployment *model.Deployment,
1583
) error {
11✔
1584
        if deployment.DeviceCount == nil {
11✔
1585
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1586
                if err != nil {
×
1587
                        return errors.Wrap(err, "counting device deployments")
×
1588
                }
×
1589
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1590
                if err != nil {
×
1591
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1592
                }
×
1593
                deployment.DeviceCount = &deviceCount
×
1594
        }
1595

1596
        return nil
11✔
1597
}
1598

1599
func (d *Deployments) LookupDeployment(ctx context.Context,
1600
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1601
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1602

1✔
1603
        if err != nil {
1✔
1604
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1605
        }
×
1606

1607
        if list == nil {
2✔
1608
                return make([]*model.Deployment, 0), 0, nil
1✔
1609
        }
1✔
1610

1611
        for _, deployment := range list {
×
1612
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1613
                        return nil, 0, err
×
1614
                }
×
1615
        }
1616

1617
        return list, totalCount, nil
×
1618
}
1619

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

1✔
1625
        // repack to temporary deployment log and validate
1✔
1626
        dlog := model.DeploymentLog{
1✔
1627
                DeviceID:     deviceID,
1✔
1628
                DeploymentID: deploymentID,
1✔
1629
                Messages:     logs,
1✔
1630
        }
1✔
1631
        if err := dlog.Validate(); err != nil {
1✔
1632
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1633
        }
×
1634

1635
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1636
                if err != nil {
×
1637
                        return err
×
1638
                } else {
×
1639
                        return ErrModelDeploymentNotFound
×
1640
                }
×
1641
        }
1642

1643
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1644
                return err
×
1645
        }
×
1646

1647
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1648
                deviceID, deploymentID, true)
1✔
1649
}
1650

1651
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1652
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1653

1✔
1654
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1655
                deviceID, deploymentID)
1✔
1656
}
1✔
1657

1658
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1659
        deploymentID string, deviceID string) (bool, error) {
1✔
1660
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1661
}
1✔
1662

1663
// AbortDeployment aborts deployment for devices and updates deployment stats
1664
func (d *Deployments) AbortDeployment(ctx context.Context, deploymentID string) error {
9✔
1665

9✔
1666
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
11✔
1667
                return err
2✔
1668
        }
2✔
1669

1670
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
7✔
1671
                ctx, deploymentID)
7✔
1672
        if err != nil {
9✔
1673
                return err
2✔
1674
        }
2✔
1675

1676
        // update statistics
1677
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
7✔
1678
                return errors.Wrap(err, "failed to update deployment stats")
2✔
1679
        }
2✔
1680

1681
        // when aborting the deployment we need to set status directly instead of
1682
        // using recalcDeploymentStatus method;
1683
        // it is possible that the deployment does not have any device deployments yet;
1684
        // in that case, all statistics are 0 and calculating status based on statistics
1685
        // will not work - the calculated status will be "pending"
1686
        if err := d.db.SetDeploymentStatus(ctx,
3✔
1687
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
3✔
1688
                return errors.Wrap(err, "failed to update deployment status")
×
1689
        }
×
1690

1691
        return nil
3✔
1692
}
1693

1694
func (d *Deployments) updateDeviceDeploymentsStatus(
1695
        ctx context.Context,
1696
        deviceId string,
1697
        status model.DeviceDeploymentStatus,
1698
) error {
30✔
1699
        var latestDeployment *time.Time
30✔
1700
        // Retrieve active device deployment for the device
30✔
1701
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
30✔
1702
        if err != nil {
34✔
1703
                return errors.Wrap(err, "Searching for active deployment for the device")
4✔
1704
        } else if deviceDeployment != nil {
34✔
1705
                now := time.Now()
4✔
1706
                ddStatus := model.DeviceDeploymentState{
4✔
1707
                        Status:     status,
4✔
1708
                        FinishTime: &now,
4✔
1709
                }
4✔
1710
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
4✔
1711
                        deviceId, ddStatus); err != nil {
4✔
1712
                        return errors.Wrap(err, "updating device deployment status")
×
1713
                }
×
1714
                latestDeployment = deviceDeployment.Created
4✔
1715
        } else {
22✔
1716
                // get latest device deployment for the device
22✔
1717
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
22✔
1718
                if err != nil {
22✔
1719
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1720
                } else if deviceDeployment == nil {
40✔
1721
                        latestDeployment = &time.Time{}
18✔
1722
                } else {
22✔
1723
                        latestDeployment = deviceDeployment.Created
4✔
1724
                }
4✔
1725
        }
1726

1727
        // get deployments newer then last device deployment
1728
        // iterate over deployments and check if the device is part of the deployment or not
1729
        // if the device is part of the deployment create new, decommisioned device deployment
1730
        for skip := 0; true; skip += 100 {
66✔
1731
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
40✔
1732
                if err != nil {
40✔
1733
                        return errors.Wrap(err, "Failed to search for newer active deployments")
×
1734
                }
×
1735
                if len(deployments) == 0 {
66✔
1736
                        break
26✔
1737
                }
1738
                for _, deployment := range deployments {
28✔
1739
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
14✔
1740
                        if err != nil {
14✔
1741
                                return err
×
1742
                        }
×
1743
                        if ok {
24✔
1744
                                _, err := d.createDeviceDeploymentWithStatus(ctx,
10✔
1745
                                        deviceId, deployment, status)
10✔
1746
                                if err != nil {
10✔
1747
                                        return err
×
1748
                                }
×
1749
                        }
1750
                }
1751
        }
1752

1753
        if err := d.reindexDevice(ctx, deviceId); err != nil {
26✔
NEW
1754
                l := log.FromContext(ctx)
×
NEW
1755
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1756
        }
×
1757

1758
        return nil
26✔
1759
}
1760

1761
// DecommissionDevice updates the status of all the pending and active deployments for a device
1762
// to decommissioned
1763
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
14✔
1764
        return d.updateDeviceDeploymentsStatus(
14✔
1765
                ctx,
14✔
1766
                deviceId,
14✔
1767
                model.DeviceDeploymentStatusDecommissioned,
14✔
1768
        )
14✔
1769
}
14✔
1770

1771
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1772
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
16✔
1773
        return d.updateDeviceDeploymentsStatus(
16✔
1774
                ctx,
16✔
1775
                deviceId,
16✔
1776
                model.DeviceDeploymentStatusAborted,
16✔
1777
        )
16✔
1778
}
16✔
1779

1780
// DeleteDeviceDeploymentsHistory deletes the device deployments history
1781
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
4✔
1782
        return d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId)
4✔
1783
}
4✔
1784

1785
// Storage settings
1786
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
5✔
1787
        settings, err := d.db.GetStorageSettings(ctx)
5✔
1788
        if err != nil {
7✔
1789
                return nil, errors.Wrap(err, "Searching for settings failed")
2✔
1790
        }
2✔
1791

1792
        return settings, nil
3✔
1793
}
1794

1795
func (d *Deployments) SetStorageSettings(
1796
        ctx context.Context,
1797
        storageSettings *model.StorageSettings,
1798
) error {
7✔
1799
        if storageSettings != nil {
14✔
1800
                ctx = storage.SettingsWithContext(ctx, storageSettings)
7✔
1801
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
7✔
1802
                        return errors.WithMessage(err,
×
1803
                                "the provided storage settings failed the health check",
×
1804
                        )
×
1805
                }
×
1806
        }
1807
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
11✔
1808
                return errors.Wrap(err, "Failed to save settings")
4✔
1809
        }
4✔
1810

1811
        return nil
3✔
1812
}
1813

1814
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
14✔
1815
        d.reportingClient = c
14✔
1816
        return d
14✔
1817
}
14✔
1818

1819
func (d *Deployments) haveReporting() bool {
12✔
1820
        return d.reportingClient != nil
12✔
1821
}
12✔
1822

1823
func (d *Deployments) search(
1824
        ctx context.Context,
1825
        tid string,
1826
        parms model.SearchParams,
1827
) ([]model.InvDevice, int, error) {
12✔
1828
        if d.haveReporting() {
14✔
1829
                return d.reportingClient.Search(ctx, tid, parms)
2✔
1830
        } else {
12✔
1831
                return d.inventoryClient.Search(ctx, tid, parms)
10✔
1832
        }
10✔
1833
}
1834

1835
func (d *Deployments) UpdateDeploymentsWithArtifactName(
1836
        ctx context.Context,
1837
        artifactName string,
1838
) error {
3✔
1839
        // first check if there are pending deployments with given artifact name
3✔
1840
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
3✔
1841
        if err != nil {
3✔
1842
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
1843
        }
×
1844
        if !exists {
4✔
1845
                return nil
1✔
1846
        }
1✔
1847

1848
        // Assign artifacts to the deployments with given artifact name
1849
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
2✔
1850
        if err != nil {
2✔
1851
                return errors.Wrap(err, "Finding artifact with given name")
×
1852
        }
×
1853

1854
        if len(artifacts) == 0 {
2✔
1855
                return ErrNoArtifact
×
1856
        }
×
1857
        artifactIDs := getArtifactIDs(artifacts)
2✔
1858
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
2✔
1859
}
1860

1861
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
47✔
1862
        if d.reportingClient != nil {
51✔
1863
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
4✔
1864
        }
4✔
1865
        return nil
43✔
1866
}
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