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

mendersoftware / deployments / 742310381

pending completion
742310381

Pull #810

gitlab-ci

Fabio Tranchitella
fix: reindex device and deployment for the `already-installed` status
Pull Request #810: MEN-5930: index device deployment objects

86 of 119 new or added lines in 2 files covered. (72.27%)

1 existing line in 1 file now uncovered.

6227 of 7964 relevant lines covered (78.19%)

76.42 hits per line

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

73.82
/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
        "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
                }
×
1087
                if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
×
1088
                        l := log.FromContext(ctx)
×
1089
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1090
                }
×
NEW
1091
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
×
NEW
1092
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
×
NEW
1093
                        l := log.FromContext(ctx)
×
NEW
1094
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1095
                }
×
UNCOV
1096
                return nil
×
1097
        }
1098

1099
        if err := d.db.AssignArtifact(
1✔
1100
                ctx,
1✔
1101
                deviceDeployment.DeviceId,
1✔
1102
                deviceDeployment.DeploymentId,
1✔
1103
                artifact,
1✔
1104
        ); err != nil {
1✔
1105
                return errors.Wrap(err, "Assigning artifact to the device deployment")
×
1106
        }
×
1107

1108
        deviceDeployment.Image = artifact
1✔
1109

1✔
1110
        return nil
1✔
1111
}
1112

1113
// Retrieves the model.Deployment and model.DeviceDeployment structures
1114
// for the device. Upon error, nil is returned for both deployment structures.
1115
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1116
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
3✔
1117

3✔
1118
        // Retrieve device deployment
3✔
1119
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
3✔
1120

3✔
1121
        if err != nil {
3✔
1122
                return nil, nil, errors.Wrap(err,
×
1123
                        "Searching for oldest active deployment for the device")
×
1124
        } else if deviceDeployment == nil {
4✔
1125
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1126
        }
1✔
1127

1128
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
3✔
1129
        if err != nil {
3✔
1130
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1131
        }
×
1132
        if deployment == nil {
3✔
1133
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1134
        }
×
1135

1136
        return deployment, deviceDeployment, nil
3✔
1137
}
1138

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

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

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

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

1189
        return nil, nil, nil
×
1190
}
1191

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

1204
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
11✔
1205
        deviceDeployment.Status = status
11✔
1206
        deviceDeployment.Active = status.Active()
11✔
1207
        deviceDeployment.Created = deployment.Created
11✔
1208

11✔
1209
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
11✔
1210
                return nil, err
×
1211
        }
×
1212

1213
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
11✔
1214
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
11✔
1215
                return nil, err
×
1216
        }
×
1217

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

1227
        deployment.Stats.Inc(status)
11✔
1228

11✔
1229
        err = d.recalcDeploymentStatus(ctx, deployment)
11✔
1230
        if err != nil {
11✔
1231
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1232
        }
×
1233

1234
        if !status.Active() {
21✔
1235
                err := d.reindexDevice(ctx, deviceID)
10✔
1236
                if err != nil {
10✔
1237
                        l := log.FromContext(ctx)
×
1238
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1239
                }
×
1240
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
10✔
1241
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
10✔
NEW
1242
                        l := log.FromContext(ctx)
×
NEW
1243
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1244
                }
×
1245
        }
1246

1247
        return deviceDeployment, nil
11✔
1248
}
1249

1250
func (d *Deployments) isDevicePartOfDeployment(
1251
        ctx context.Context,
1252
        deviceID string,
1253
        deployment *model.Deployment,
1254
) (bool, error) {
15✔
1255
        for _, id := range deployment.DeviceList {
26✔
1256
                if id == deviceID {
22✔
1257
                        return true, nil
11✔
1258
                }
11✔
1259
        }
1260
        return false, nil
5✔
1261
}
1262

1263
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1264
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1265
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
3✔
1266

3✔
1267
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
3✔
1268
        if err != nil {
3✔
1269
                return nil, ErrModelInternal
×
1270
        } else if deployment == nil {
4✔
1271
                return nil, nil
1✔
1272
        }
1✔
1273

1274
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
3✔
1275
        if err != nil {
4✔
1276
                return nil, err
1✔
1277
        }
1✔
1278

1279
        // if the deployment is not forcing the installation, and if the device
1280
        // reported same artifact name as the one in the device deployment, and this is
1281
        // a new device deployment - indicated by device deployment status "pending",
1282
        // pretend there is no deployment for this device, but update
1283
        // its status to already installed first
1284
        if !deployment.ForceInstallation &&
3✔
1285
                request.DeviceProvides.ArtifactName != "" &&
3✔
1286
                deployment.ArtifactName == request.DeviceProvides.ArtifactName &&
3✔
1287
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
6✔
1288

3✔
1289
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
3✔
1290
                        model.DeviceDeploymentState{
3✔
1291
                                Status: model.DeviceDeploymentStatusAlreadyInst,
3✔
1292
                        }); err != nil {
3✔
1293
                        return nil, errors.Wrap(err, "Failed to update deployment status")
×
1294
                }
×
1295
                if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
3✔
NEW
1296
                        l := log.FromContext(ctx)
×
NEW
1297
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1298
                }
×
1299
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
3✔
1300
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
3✔
NEW
1301
                        l := log.FromContext(ctx)
×
NEW
1302
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1303
                }
×
1304

1305
                return nil, nil
3✔
1306
        }
1307

1308
        if deployment.Type == model.DeploymentTypeConfiguration {
2✔
1309
                // There's nothing more we need to do, the link must be filled
1✔
1310
                // in by the API layer.
1✔
1311
                return &model.DeploymentInstructions{
1✔
1312
                        ID: deployment.Id,
1✔
1313
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1314
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1315
                                // use deployment ID togheter with device ID as artifact ID
1✔
1316
                                ID:                    deployment.Id + deviceID,
1✔
1317
                                ArtifactName:          deployment.ArtifactName,
1✔
1318
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1319
                        },
1✔
1320
                        Type: model.DeploymentTypeConfiguration,
1✔
1321
                }, nil
1✔
1322
        }
1✔
1323

1324
        // assign artifact only if the artifact was not assigned previously
1325
        if deviceDeployment.Image == nil {
2✔
1326
                if err := d.assignArtifact(
1✔
1327
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
1✔
1328
                        return nil, err
×
1329
                }
×
1330
        }
1331

1332
        if deviceDeployment.Image == nil {
1✔
1333
                return nil, nil
×
1334
        }
×
1335

1336
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
1337
        if err != nil {
1✔
1338
                return nil, err
×
1339
        }
×
1340

1341
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1342
        link, err := d.objectStorage.GetRequest(
1✔
1343
                ctx,
1✔
1344
                imagePath,
1✔
1345
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1346
                DefaultUpdateDownloadLinkExpire,
1✔
1347
        )
1✔
1348
        if err != nil {
1✔
1349
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1350
        }
×
1351

1352
        instructions := &model.DeploymentInstructions{
1✔
1353
                ID: deviceDeployment.DeploymentId,
1✔
1354
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1355
                        ID: deviceDeployment.Image.Id,
1✔
1356
                        ArtifactName: deviceDeployment.Image.
1✔
1357
                                ArtifactMeta.Name,
1✔
1358
                        Source: *link,
1✔
1359
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1360
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1361
                },
1✔
1362
        }
1✔
1363

1✔
1364
        return instructions, nil
1✔
1365
}
1366

1367
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1368
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
3✔
1369
        if deviceDeployment.Request != nil {
4✔
1370
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1371
                        // the device reported different device type and/or artifact name
1✔
1372
                        // during the update process, which should never happen;
1✔
1373
                        // mark deployment for this device as failed to force client to rollback
1✔
1374
                        l := log.FromContext(ctx)
1✔
1375
                        l.Errorf(
1✔
1376
                                "Device with id %s reported new data: %s during update process;"+
1✔
1377
                                        "old data: %s",
1✔
1378
                                deviceID, request, deviceDeployment.Request)
1✔
1379

1✔
1380
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1381
                                model.DeviceDeploymentState{
1✔
1382
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1383
                                }); err != nil {
1✔
NEW
1384
                                return errors.Wrap(err, "Failed to update deployment status")
×
NEW
1385
                        }
×
1386
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
NEW
1387
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1388
                        }
×
1389
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1390
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
NEW
1391
                                l := log.FromContext(ctx)
×
NEW
1392
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1393
                        }
×
1394
                        return ErrConflictingRequestData
1✔
1395
                }
1396
        } else {
3✔
1397
                // save the request
3✔
1398
                if err := d.db.SaveDeviceDeploymentRequest(
3✔
1399
                        ctx, deviceDeployment.Id, request); err != nil {
3✔
NEW
1400
                        return err
×
NEW
1401
                }
×
1402
        }
1403
        return nil
3✔
1404
}
1405

1406
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1407
// ID `deviceID`. Returns nil if update was successful.
1408
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1409
        deviceID string, ddState model.DeviceDeploymentState) error {
11✔
1410

11✔
1411
        l := log.FromContext(ctx)
11✔
1412

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

11✔
1415
        var finishTime *time.Time = nil
11✔
1416
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
18✔
1417
                now := time.Now()
7✔
1418
                finishTime = &now
7✔
1419
        }
7✔
1420

1421
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
11✔
1422
        if err == mongo.ErrStorageNotFound {
13✔
1423
                return ErrStorageNotFound
2✔
1424
        } else if err != nil {
11✔
1425
                return err
×
1426
        }
×
1427

1428
        currentStatus := dd.Status
9✔
1429

9✔
1430
        if currentStatus == model.DeviceDeploymentStatusAborted {
9✔
1431
                return ErrDeploymentAborted
×
1432
        }
×
1433

1434
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
9✔
1435
                return ErrDeviceDecommissioned
×
1436
        }
×
1437

1438
        // nothing to do
1439
        if ddState.Status == currentStatus {
9✔
1440
                return nil
×
1441
        }
×
1442

1443
        // update finish time
1444
        ddState.FinishTime = finishTime
9✔
1445

9✔
1446
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
9✔
1447
                deviceID, deploymentID, ddState)
9✔
1448
        if err != nil {
9✔
1449
                return err
×
1450
        }
×
1451

1452
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
9✔
1453
                return err
×
1454
        }
×
1455

1456
        // fetch deployment stats and update deployment status
1457
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
9✔
1458
        if err != nil {
9✔
1459
                return errors.Wrap(err, "failed when searching for deployment")
×
1460
        }
×
1461

1462
        err = d.recalcDeploymentStatus(ctx, deployment)
9✔
1463
        if err != nil {
9✔
1464
                return errors.Wrap(err, "failed to update deployment status")
×
1465
        }
×
1466

1467
        if !ddState.Status.Active() {
16✔
1468
                if err := d.reindexDevice(ctx, deviceID); err != nil {
7✔
1469
                        l := log.FromContext(ctx)
×
1470
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1471
                }
×
1472
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
7✔
NEW
1473
                        l := log.FromContext(ctx)
×
NEW
1474
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1475
                }
×
1476
        }
1477

1478
        return nil
9✔
1479
}
1480

1481
// recalcDeploymentStatus inspects the deployment stats and
1482
// recalculates and updates its status
1483
// it should be used whenever deployment stats are touched
1484
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
19✔
1485
        status := dep.GetStatus()
19✔
1486

19✔
1487
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
19✔
1488
                return err
×
1489
        }
×
1490

1491
        return nil
19✔
1492
}
1493

1494
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1495
        deploymentID string) (model.Stats, error) {
1✔
1496

1✔
1497
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1498

1✔
1499
        if err != nil {
1✔
1500
                return nil, errors.Wrap(err, "checking deployment id")
×
1501
        }
×
1502

1503
        if deployment == nil {
1✔
1504
                return nil, nil
×
1505
        }
×
1506

1507
        return deployment.Stats, nil
1✔
1508
}
1509
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1510
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1511

×
1512
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1513

×
1514
        if err != nil {
×
1515
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1516
        }
×
1517

1518
        if deploymentStats == nil {
×
1519
                return nil, ErrModelDeploymentNotFound
×
1520
        }
×
1521

1522
        return deploymentStats, nil
×
1523
}
1524

1525
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1526
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1527
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1528

1✔
1529
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1530
        if err != nil {
1✔
1531
                return nil, ErrModelInternal
×
1532
        }
×
1533

1534
        if deployment == nil {
1✔
1535
                return nil, ErrModelDeploymentNotFound
×
1536
        }
×
1537

1538
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1539
        if err != nil {
1✔
1540
                return nil, ErrModelInternal
×
1541
        }
×
1542

1543
        return statuses, nil
1✔
1544
}
1545

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

1✔
1549
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1550
        if err != nil {
1✔
1551
                return nil, -1, ErrModelInternal
×
1552
        }
×
1553

1554
        if deployment == nil {
1✔
1555
                return nil, -1, ErrModelDeploymentNotFound
×
1556
        }
×
1557

1558
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1559
        if err != nil {
1✔
1560
                return nil, -1, ErrModelInternal
×
1561
        }
×
1562

1563
        return statuses, totalCount, nil
1✔
1564
}
1565

1566
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1567
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
8✔
1568
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
8✔
1569
        if err != nil {
10✔
1570
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
2✔
1571
        }
2✔
1572

1573
        deploymentIDs := make([]string, len(deviceDeployments))
6✔
1574
        for i, deviceDeployment := range deviceDeployments {
18✔
1575
                deploymentIDs[i] = deviceDeployment.DeploymentId
12✔
1576
        }
12✔
1577

1578
        deployments, _, err := d.db.Find(ctx, model.Query{
6✔
1579
                IDs:          deploymentIDs,
6✔
1580
                Limit:        len(deviceDeployments),
6✔
1581
                DisableCount: true,
6✔
1582
        })
6✔
1583
        if err != nil {
8✔
1584
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
2✔
1585
        }
2✔
1586

1587
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
4✔
1588
        for _, deployment := range deployments {
10✔
1589
                deploymentsMap[deployment.Id] = deployment
6✔
1590
        }
6✔
1591

1592
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
4✔
1593
        for _, deviceDeployment := range deviceDeployments {
12✔
1594
                device := model.DeviceDeploymentWithImage(deviceDeployment)
8✔
1595
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
14✔
1596
                        res = append(res, model.DeviceDeploymentListItem{
6✔
1597
                                Id:         deviceDeployment.Id,
6✔
1598
                                Deployment: deployment,
6✔
1599
                                Device:     &device,
6✔
1600
                        })
6✔
1601
                } else {
8✔
1602
                        res = append(res, model.DeviceDeploymentListItem{
2✔
1603
                                Id:     deviceDeployment.Id,
2✔
1604
                                Device: &device,
2✔
1605
                        })
2✔
1606

2✔
1607
                }
2✔
1608
        }
1609

1610
        return res, totalCount, nil
4✔
1611
}
1612

1613
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1614
        ctx context.Context,
1615
        deployment *model.Deployment,
1616
) error {
11✔
1617
        if deployment.DeviceCount == nil {
11✔
1618
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1619
                if err != nil {
×
1620
                        return errors.Wrap(err, "counting device deployments")
×
1621
                }
×
1622
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1623
                if err != nil {
×
1624
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1625
                }
×
1626
                deployment.DeviceCount = &deviceCount
×
1627
        }
1628

1629
        return nil
11✔
1630
}
1631

1632
func (d *Deployments) LookupDeployment(ctx context.Context,
1633
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1634
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1635

1✔
1636
        if err != nil {
1✔
1637
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1638
        }
×
1639

1640
        if list == nil {
2✔
1641
                return make([]*model.Deployment, 0), 0, nil
1✔
1642
        }
1✔
1643

1644
        for _, deployment := range list {
×
1645
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1646
                        return nil, 0, err
×
1647
                }
×
1648
        }
1649

1650
        return list, totalCount, nil
×
1651
}
1652

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

1✔
1658
        // repack to temporary deployment log and validate
1✔
1659
        dlog := model.DeploymentLog{
1✔
1660
                DeviceID:     deviceID,
1✔
1661
                DeploymentID: deploymentID,
1✔
1662
                Messages:     logs,
1✔
1663
        }
1✔
1664
        if err := dlog.Validate(); err != nil {
1✔
1665
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1666
        }
×
1667

1668
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1669
                if err != nil {
×
1670
                        return err
×
1671
                } else {
×
1672
                        return ErrModelDeploymentNotFound
×
1673
                }
×
1674
        }
1675

1676
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1677
                return err
×
1678
        }
×
1679

1680
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1681
                deviceID, deploymentID, true)
1✔
1682
}
1683

1684
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1685
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1686

1✔
1687
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1688
                deviceID, deploymentID)
1✔
1689
}
1✔
1690

1691
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1692
        deploymentID string, deviceID string) (bool, error) {
1✔
1693
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1694
}
1✔
1695

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

9✔
1699
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
11✔
1700
                return err
2✔
1701
        }
2✔
1702

1703
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
7✔
1704
                ctx, deploymentID)
7✔
1705
        if err != nil {
9✔
1706
                return err
2✔
1707
        }
2✔
1708

1709
        // update statistics
1710
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
7✔
1711
                return errors.Wrap(err, "failed to update deployment stats")
2✔
1712
        }
2✔
1713

1714
        // when aborting the deployment we need to set status directly instead of
1715
        // using recalcDeploymentStatus method;
1716
        // it is possible that the deployment does not have any device deployments yet;
1717
        // in that case, all statistics are 0 and calculating status based on statistics
1718
        // will not work - the calculated status will be "pending"
1719
        if err := d.db.SetDeploymentStatus(ctx,
3✔
1720
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
3✔
1721
                return errors.Wrap(err, "failed to update deployment status")
×
1722
        }
×
1723

1724
        return nil
3✔
1725
}
1726

1727
func (d *Deployments) updateDeviceDeploymentsStatus(
1728
        ctx context.Context,
1729
        deviceId string,
1730
        status model.DeviceDeploymentStatus,
1731
) error {
30✔
1732
        var latestDeployment *time.Time
30✔
1733
        // Retrieve active device deployment for the device
30✔
1734
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
30✔
1735
        if err != nil {
34✔
1736
                return errors.Wrap(err, "Searching for active deployment for the device")
4✔
1737
        } else if deviceDeployment != nil {
34✔
1738
                now := time.Now()
4✔
1739
                ddStatus := model.DeviceDeploymentState{
4✔
1740
                        Status:     status,
4✔
1741
                        FinishTime: &now,
4✔
1742
                }
4✔
1743
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
4✔
1744
                        deviceId, ddStatus); err != nil {
4✔
1745
                        return errors.Wrap(err, "updating device deployment status")
×
1746
                }
×
1747
                latestDeployment = deviceDeployment.Created
4✔
1748
        } else {
22✔
1749
                // get latest device deployment for the device
22✔
1750
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
22✔
1751
                if err != nil {
22✔
1752
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1753
                } else if deviceDeployment == nil {
40✔
1754
                        latestDeployment = &time.Time{}
18✔
1755
                } else {
22✔
1756
                        latestDeployment = deviceDeployment.Created
4✔
1757
                }
4✔
1758
        }
1759

1760
        // get deployments newer then last device deployment
1761
        // iterate over deployments and check if the device is part of the deployment or not
1762
        // if the device is part of the deployment create new, decommisioned device deployment
1763
        for skip := 0; true; skip += 100 {
66✔
1764
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
40✔
1765
                if err != nil {
40✔
1766
                        return errors.Wrap(err, "Failed to search for newer active deployments")
×
1767
                }
×
1768
                if len(deployments) == 0 {
66✔
1769
                        break
26✔
1770
                }
1771
                for _, deployment := range deployments {
28✔
1772
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
14✔
1773
                        if err != nil {
14✔
1774
                                return err
×
1775
                        }
×
1776
                        if ok {
24✔
1777
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
10✔
1778
                                        deviceId, deployment, status)
10✔
1779
                                if err != nil {
10✔
1780
                                        return err
×
1781
                                }
×
1782
                                if !status.Active() {
20✔
1783
                                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
10✔
1784
                                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
10✔
NEW
1785
                                                l := log.FromContext(ctx)
×
NEW
1786
                                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
NEW
1787
                                        }
×
1788
                                }
1789
                        }
1790
                }
1791
        }
1792

1793
        if err := d.reindexDevice(ctx, deviceId); err != nil {
26✔
1794
                l := log.FromContext(ctx)
×
1795
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1796
        }
×
1797

1798
        return nil
26✔
1799
}
1800

1801
// DecommissionDevice updates the status of all the pending and active deployments for a device
1802
// to decommissioned
1803
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
14✔
1804
        return d.updateDeviceDeploymentsStatus(
14✔
1805
                ctx,
14✔
1806
                deviceId,
14✔
1807
                model.DeviceDeploymentStatusDecommissioned,
14✔
1808
        )
14✔
1809
}
14✔
1810

1811
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1812
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
16✔
1813
        return d.updateDeviceDeploymentsStatus(
16✔
1814
                ctx,
16✔
1815
                deviceId,
16✔
1816
                model.DeviceDeploymentStatusAborted,
16✔
1817
        )
16✔
1818
}
16✔
1819

1820
// DeleteDeviceDeploymentsHistory deletes the device deployments history
1821
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
4✔
1822
        return d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId)
4✔
1823
}
4✔
1824

1825
// Storage settings
1826
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
5✔
1827
        settings, err := d.db.GetStorageSettings(ctx)
5✔
1828
        if err != nil {
7✔
1829
                return nil, errors.Wrap(err, "Searching for settings failed")
2✔
1830
        }
2✔
1831

1832
        return settings, nil
3✔
1833
}
1834

1835
func (d *Deployments) SetStorageSettings(
1836
        ctx context.Context,
1837
        storageSettings *model.StorageSettings,
1838
) error {
7✔
1839
        if storageSettings != nil {
14✔
1840
                ctx = storage.SettingsWithContext(ctx, storageSettings)
7✔
1841
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
7✔
1842
                        return errors.WithMessage(err,
×
1843
                                "the provided storage settings failed the health check",
×
1844
                        )
×
1845
                }
×
1846
        }
1847
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
11✔
1848
                return errors.Wrap(err, "Failed to save settings")
4✔
1849
        }
4✔
1850

1851
        return nil
3✔
1852
}
1853

1854
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
15✔
1855
        d.reportingClient = c
15✔
1856
        return d
15✔
1857
}
15✔
1858

1859
func (d *Deployments) haveReporting() bool {
12✔
1860
        return d.reportingClient != nil
12✔
1861
}
12✔
1862

1863
func (d *Deployments) search(
1864
        ctx context.Context,
1865
        tid string,
1866
        parms model.SearchParams,
1867
) ([]model.InvDevice, int, error) {
12✔
1868
        if d.haveReporting() {
14✔
1869
                return d.reportingClient.Search(ctx, tid, parms)
2✔
1870
        } else {
12✔
1871
                return d.inventoryClient.Search(ctx, tid, parms)
10✔
1872
        }
10✔
1873
}
1874

1875
func (d *Deployments) UpdateDeploymentsWithArtifactName(
1876
        ctx context.Context,
1877
        artifactName string,
1878
) error {
3✔
1879
        // first check if there are pending deployments with given artifact name
3✔
1880
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
3✔
1881
        if err != nil {
3✔
1882
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
1883
        }
×
1884
        if !exists {
4✔
1885
                return nil
1✔
1886
        }
1✔
1887

1888
        // Assign artifacts to the deployments with given artifact name
1889
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
2✔
1890
        if err != nil {
2✔
1891
                return errors.Wrap(err, "Finding artifact with given name")
×
1892
        }
×
1893

1894
        if len(artifacts) == 0 {
2✔
1895
                return ErrNoArtifact
×
1896
        }
×
1897
        artifactIDs := getArtifactIDs(artifacts)
2✔
1898
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
2✔
1899
}
1900

1901
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
49✔
1902
        if d.reportingClient != nil {
54✔
1903
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
5✔
1904
        }
5✔
1905
        return nil
44✔
1906
}
1907

1908
func (d *Deployments) reindexDeployment(ctx context.Context,
1909
        deviceID, deploymentID, ID string) error {
33✔
1910
        if d.reportingClient != nil {
38✔
1911
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
5✔
1912
        }
5✔
1913
        return nil
28✔
1914
}
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