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

mendersoftware / deployments / 856345526

pending completion
856345526

Pull #860

gitlab-ci

Peter Grzybowski
chore: api docs check fix
Pull Request #860: chore: api docs check fix

7044 of 8880 relevant lines covered (79.32%)

69.7 hits per line

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

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

15
package app
16

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

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

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

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

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

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

61
        fileSuffixTmp = ".tmp"
62

63
        inprogressIdleTime = time.Hour
64
)
65

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

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

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

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

104
//deployments
105

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

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

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

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

186
type Deployments struct {
187
        db              store.DataStore
188
        objectStorage   storage.ObjectStorage
189
        workflowsClient workflows.Client
190
        inventoryClient inventory.Client
191
        reportingClient reporting.Client
192
}
193

194
// Compile-time check
195
var _ App = &Deployments{}
196

197
func NewDeployments(
198
        storage store.DataStore,
199
        objectStorage storage.ObjectStorage,
200
) *Deployments {
121✔
201
        return &Deployments{
121✔
202
                db:              storage,
121✔
203
                objectStorage:   objectStorage,
121✔
204
                workflowsClient: workflows.NewClient(),
121✔
205
                inventoryClient: inventory.NewClient(),
121✔
206
        }
121✔
207
}
121✔
208

209
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
8✔
210
        d.workflowsClient = workflowsClient
8✔
211
}
8✔
212

213
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
16✔
214
        d.inventoryClient = inventoryClient
16✔
215
}
16✔
216

217
func (d *Deployments) HealthCheck(ctx context.Context) error {
12✔
218
        err := d.db.Ping(ctx)
12✔
219
        if err != nil {
14✔
220
                return errors.Wrap(err, "error reaching MongoDB")
2✔
221
        }
2✔
222
        err = d.objectStorage.HealthCheck(ctx)
10✔
223
        if err != nil {
12✔
224
                return errors.Wrap(
2✔
225
                        err,
2✔
226
                        "error reaching artifact storage service",
2✔
227
                )
2✔
228
        }
2✔
229

230
        err = d.workflowsClient.CheckHealth(ctx)
8✔
231
        if err != nil {
10✔
232
                return errors.Wrap(err, "Workflows service unhealthy")
2✔
233
        }
2✔
234

235
        err = d.inventoryClient.CheckHealth(ctx)
6✔
236
        if err != nil {
8✔
237
                return errors.Wrap(err, "Inventory service unhealthy")
2✔
238
        }
2✔
239

240
        if d.reportingClient != nil {
8✔
241
                err = d.reportingClient.CheckHealth(ctx)
4✔
242
                if err != nil {
6✔
243
                        return errors.Wrap(err, "Reporting service unhealthy")
2✔
244
                }
2✔
245
        }
246
        return nil
2✔
247
}
248

249
func (d *Deployments) contextWithStorageSettings(
250
        ctx context.Context,
251
) (context.Context, error) {
43✔
252
        var err error
43✔
253
        settings, ok := storage.SettingsFromContext(ctx)
43✔
254
        if !ok {
82✔
255
                settings, err = d.db.GetStorageSettings(ctx)
39✔
256
        }
39✔
257
        if err != nil {
47✔
258
                return nil, err
4✔
259
        } else if settings != nil {
43✔
260
                err = settings.Validate()
×
261
                if err != nil {
×
262
                        return nil, err
×
263
                }
×
264
        }
265
        return storage.SettingsWithContext(ctx, settings), nil
39✔
266
}
267

268
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
6✔
269
        limit, err := d.db.GetLimit(ctx, name)
6✔
270
        if err == mongo.ErrLimitNotFound {
8✔
271
                return &model.Limit{
2✔
272
                        Name:  name,
2✔
273
                        Value: 0,
2✔
274
                }, nil
2✔
275

2✔
276
        } else if err != nil {
8✔
277
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
2✔
278
        }
2✔
279
        return limit, nil
2✔
280
}
281

282
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
5✔
283
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
7✔
284
                return errors.Wrap(err, "failed to provision tenant")
2✔
285
        }
2✔
286

287
        return nil
3✔
288
}
289

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

298
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
299
// and creates image structure in the system.
300
// Returns image ID, artifact file ID and nil on success.
301
func (d *Deployments) handleArtifact(ctx context.Context,
302
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
5✔
303

5✔
304
        l := log.FromContext(ctx)
5✔
305
        ctx, err := d.contextWithStorageSettings(ctx)
5✔
306
        if err != nil {
5✔
307
                return "", err
×
308
        }
×
309

310
        // create pipe
311
        pR, pW := io.Pipe()
5✔
312

5✔
313
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
5✔
314

5✔
315
        tee := io.TeeReader(artifactReader, pW)
5✔
316

5✔
317
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
5✔
318
        if err != nil {
6✔
319
                uid, _ = uuid.NewRandom()
1✔
320
        }
1✔
321
        artifactID := uid.String()
5✔
322

5✔
323
        ch := make(chan error)
5✔
324
        // create goroutine for artifact upload
5✔
325
        //
5✔
326
        // reading from the pipe (which is done in UploadArtifact method) is a blocking operation
5✔
327
        // and cannot be done in the same goroutine as writing to the pipe
5✔
328
        //
5✔
329
        // uploading and parsing artifact in the same process will cause in a deadlock!
5✔
330
        //nolint:errcheck
5✔
331
        go func() (err error) {
10✔
332
                defer func() { ch <- err }()
10✔
333
                err = d.objectStorage.PutObject(
5✔
334
                        ctx, model.ImagePathFromContext(ctx, artifactID), pR,
5✔
335
                )
5✔
336
                if err != nil {
6✔
337
                        pR.CloseWithError(err)
1✔
338
                }
1✔
339
                return err
5✔
340
        }()
341

342
        // parse artifact
343
        // artifact library reads all the data from the given reader
344
        metaArtifactConstructor, err := getMetaFromArchive(&tee)
5✔
345
        if err != nil {
10✔
346
                _ = pW.CloseWithError(err)
5✔
347
                <-ch
5✔
348
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
5✔
349
        }
5✔
350
        // validate artifact metadata
351
        if err = metaArtifactConstructor.Validate(); err != nil {
1✔
352
                return artifactID, ErrModelInvalidMetadata
×
353
        }
×
354

355
        // read the rest of the data,
356
        // just in case the artifact library did not read all the data from the reader
357
        _, err = io.Copy(io.Discard, tee)
1✔
358
        if err != nil {
1✔
359
                // CloseWithError will cause the reading end to abort upload.
×
360
                _ = pW.CloseWithError(err)
×
361
                <-ch
×
362
                return artifactID, err
×
363
        }
×
364

365
        // close the pipe
366
        pW.Close()
1✔
367

1✔
368
        // collect output from the goroutine
1✔
369
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
370
                return artifactID, uploadResponseErr
×
371
        }
×
372

373
        image := model.NewImage(
1✔
374
                artifactID,
1✔
375
                multipartUploadMsg.MetaConstructor,
1✔
376
                metaArtifactConstructor,
1✔
377
                artifactReader.Count(),
1✔
378
        )
1✔
379

1✔
380
        // save image structure in the system
1✔
381
        if err = d.db.InsertImage(ctx, image); err != nil {
1✔
382
                // Try to remove the storage from s3.
×
383
                if errDelete := d.objectStorage.DeleteObject(
×
384
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
385
                ); errDelete != nil {
×
386
                        l.Errorf(
×
387
                                "failed to clean up artifact storage after failure: %s",
×
388
                                errDelete,
×
389
                        )
×
390
                }
×
391
                if idxErr, ok := err.(*model.ConflictError); ok {
×
392
                        return artifactID, idxErr
×
393
                }
×
394
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
395
        }
396
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
397
                return "", errors.Wrap(err, "fail to update deployments")
×
398
        }
×
399

400
        return artifactID, nil
1✔
401
}
402

403
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
404
// creates image structure in the system, and starts the workflow to generate the
405
// artifact from them.
406
// Returns image ID and nil on success.
407
func (d *Deployments) GenerateImage(ctx context.Context,
408
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
21✔
409

21✔
410
        if multipartGenerateImageMsg == nil {
23✔
411
                return "", ErrModelMultipartUploadMsgMalformed
2✔
412
        }
2✔
413

414
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
19✔
415
        if err != nil {
29✔
416
                return "", err
10✔
417
        }
10✔
418
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
11✔
419
                multipartGenerateImageMsg.TenantID = id.Tenant
2✔
420
        }
2✔
421
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
9✔
422
        if err != nil {
13✔
423
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
6✔
424
                        return "", errors.Wrap(err, cleanupErr.Error())
2✔
425
                }
2✔
426
                return "", err
2✔
427
        }
428

429
        return multipartGenerateImageMsg.ArtifactID, err
5✔
430
}
431

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

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

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

478
// handleRawFile parses raw data, uploads it to the file storage,
479
// and starts the workflow to generate the artifact.
480
// Returns the object path to the file and nil on success.
481
func (d *Deployments) handleRawFile(ctx context.Context,
482
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
19✔
483
        l := log.FromContext(ctx)
19✔
484
        uid, _ := uuid.NewRandom()
19✔
485
        artifactID := uid.String()
19✔
486
        multipartMsg.ArtifactID = artifactID
19✔
487
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
19✔
488

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

503
        ctx, err = d.contextWithStorageSettings(ctx)
15✔
504
        if err != nil {
15✔
505
                return "", err
×
506
        }
×
507
        err = d.objectStorage.PutObject(
15✔
508
                ctx, filePath, multipartMsg.FileReader,
15✔
509
        )
15✔
510
        if err != nil {
17✔
511
                return "", err
2✔
512
        }
2✔
513
        defer func() {
26✔
514
                if err != nil {
17✔
515
                        e := d.objectStorage.DeleteObject(ctx, filePath)
4✔
516
                        if e != nil {
8✔
517
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
4✔
518
                                        filePath, e)
4✔
519
                        }
4✔
520
                }
521
        }()
522

523
        link, err := d.objectStorage.GetRequest(
13✔
524
                ctx,
13✔
525
                filePath,
13✔
526
                path.Base(filePath),
13✔
527
                DefaultImageGenerationLinkExpire,
13✔
528
        )
13✔
529
        if err != nil {
15✔
530
                return "", err
2✔
531
        }
2✔
532
        multipartMsg.GetArtifactURI = link.Uri
11✔
533

11✔
534
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
11✔
535
        if err != nil {
13✔
536
                return "", err
2✔
537
        }
2✔
538
        multipartMsg.DeleteArtifactURI = link.Uri
9✔
539

9✔
540
        return artifactID, nil
9✔
541
}
542

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

1✔
547
        image, err := d.db.FindImageByID(ctx, id)
1✔
548
        if err != nil {
1✔
549
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
550
        }
×
551

552
        if image == nil {
2✔
553
                return nil, nil
1✔
554
        }
1✔
555

556
        return image, nil
1✔
557
}
558

559
// DeleteImage removes metadata and image file
560
// Noop for not existing images
561
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
562
// file is needed
563
// In case of already finished updates only image file is not needed, metadata is attached directly
564
// to device deployment therefore we still have some information about image that have been used
565
// (but not the file)
566
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
1✔
567
        found, err := d.GetImage(ctx, imageID)
1✔
568

1✔
569
        if err != nil {
1✔
570
                return errors.Wrap(err, "Getting image metadata")
×
571
        }
×
572

573
        if found == nil {
1✔
574
                return ErrImageMetaNotFound
×
575
        }
×
576

577
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
1✔
578
        if err != nil {
1✔
579
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
580
        }
×
581

582
        // Image is in use, not allowed to delete
583
        if inUse {
2✔
584
                return ErrModelImageInActiveDeployment
1✔
585
        }
1✔
586

587
        // Delete image file (call to external service)
588
        // Noop for not existing file
589
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
590
        if err != nil {
1✔
591
                return err
×
592
        }
×
593
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
594
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
1✔
595
                return errors.Wrap(err, "Deleting image file")
×
596
        }
×
597

598
        // Delete metadata
599
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
600
                return errors.Wrap(err, "Deleting image metadata")
×
601
        }
×
602

603
        return nil
1✔
604
}
605

606
// ListImages according to specified filers.
607
func (d *Deployments) ListImages(
608
        ctx context.Context,
609
        filters *model.ReleaseOrImageFilter,
610
) ([]*model.Image, int, error) {
1✔
611
        imageList, count, err := d.db.ListImages(ctx, filters)
1✔
612
        if err != nil {
1✔
613
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
614
        }
×
615

616
        if imageList == nil {
2✔
617
                return make([]*model.Image, 0), 0, nil
1✔
618
        }
1✔
619

620
        return imageList, count, nil
1✔
621
}
622

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

×
627
        if err := constructor.Validate(); err != nil {
×
628
                return false, errors.Wrap(err, "Validating image metadata")
×
629
        }
×
630

631
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
632
        if err != nil {
×
633
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
634
        }
×
635

636
        if found {
×
637
                return false, ErrModelImageUsedInAnyDeployment
×
638
        }
×
639

640
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
641
        if err != nil {
×
642
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
643
        }
×
644

645
        if foundImage == nil {
×
646
                return false, nil
×
647
        }
×
648

649
        foundImage.SetModified(time.Now())
×
650
        foundImage.ImageMeta = constructor
×
651

×
652
        _, err = d.db.Update(ctx, foundImage)
×
653
        if err != nil {
×
654
                return false, errors.Wrap(err, "Updating image matadata")
×
655
        }
×
656

657
        return true, nil
×
658
}
659

660
// DownloadLink presigned GET link to download image file.
661
// Returns error if image have not been uploaded.
662
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
663
        expire time.Duration) (*model.Link, error) {
1✔
664

1✔
665
        image, err := d.GetImage(ctx, imageID)
1✔
666
        if err != nil {
1✔
667
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
668
        }
×
669

670
        if image == nil {
1✔
671
                return nil, nil
×
672
        }
×
673

674
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
675
        if err != nil {
1✔
676
                return nil, err
×
677
        }
×
678
        imagePath := model.ImagePathFromContext(ctx, imageID)
1✔
679
        _, err = d.objectStorage.StatObject(ctx, imagePath)
1✔
680
        if err != nil {
1✔
681
                return nil, errors.Wrap(err, "Searching for image file")
×
682
        }
×
683

684
        link, err := d.objectStorage.GetRequest(
1✔
685
                ctx,
1✔
686
                imagePath,
1✔
687
                image.Name+model.ArtifactFileSuffix,
1✔
688
                expire,
1✔
689
        )
1✔
690
        if err != nil {
1✔
691
                return nil, errors.Wrap(err, "Generating download link")
×
692
        }
×
693

694
        return link, nil
1✔
695
}
696

697
func (d *Deployments) UploadLink(
698
        ctx context.Context,
699
        expire time.Duration,
700
) (*model.UploadLink, error) {
11✔
701
        ctx, err := d.contextWithStorageSettings(ctx)
11✔
702
        if err != nil {
13✔
703
                return nil, err
2✔
704
        }
2✔
705

706
        artifactID := uuid.New().String()
9✔
707
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
9✔
708
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
9✔
709
        if err != nil {
11✔
710
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
2✔
711
        }
2✔
712
        upLink := &model.UploadLink{
7✔
713
                ArtifactID: artifactID,
7✔
714
                IssuedAt:   time.Now(),
7✔
715
                Link:       *link,
7✔
716
        }
7✔
717
        err = d.db.InsertUploadIntent(ctx, upLink)
7✔
718
        if err != nil {
9✔
719
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
2✔
720
        }
2✔
721

722
        return upLink, err
5✔
723
}
724

725
func (d *Deployments) processUploadedArtifact(
726
        ctx context.Context,
727
        artifactID string,
728
        artifact io.ReadCloser) error {
5✔
729
        linkStatus := model.LinkStatusCompleted
5✔
730

5✔
731
        l := log.FromContext(ctx)
5✔
732
        defer artifact.Close()
5✔
733
        ctx, cancel := context.WithCancel(ctx)
5✔
734
        defer cancel()
5✔
735
        go func() { // Heatbeat routine
10✔
736
                ticker := time.NewTicker(inprogressIdleTime / 2)
5✔
737
                done := ctx.Done()
5✔
738
                defer ticker.Stop()
5✔
739
                for {
10✔
740
                        select {
5✔
741
                        case <-ticker.C:
×
742
                                err := d.db.UpdateUploadIntentStatus(
×
743
                                        ctx,
×
744
                                        artifactID,
×
745
                                        model.LinkStatusProcessing,
×
746
                                        model.LinkStatusProcessing,
×
747
                                )
×
748
                                if err != nil {
×
749
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
750
                                        cancel()
×
751
                                        return
×
752
                                }
×
753
                        case <-done:
5✔
754
                                return
5✔
755
                        }
756
                }
757
        }()
758
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
5✔
759
                ArtifactID:     artifactID,
5✔
760
                ArtifactReader: artifact,
5✔
761
        })
5✔
762
        if err != nil {
9✔
763
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
4✔
764
                linkStatus = model.LinkStatusAborted
4✔
765
        }
4✔
766
        errDB := d.db.UpdateUploadIntentStatus(
5✔
767
                ctx, artifactID,
5✔
768
                model.LinkStatusProcessing, linkStatus,
5✔
769
        )
5✔
770
        if errDB != nil {
7✔
771
                l.Warnf("failed to update upload link status: %s", errDB)
2✔
772
        }
2✔
773
        return err
5✔
774
}
775

776
func (d *Deployments) CompleteUpload(
777
        ctx context.Context,
778
        intentID string,
779
) error {
15✔
780
        l := log.FromContext(ctx)
15✔
781
        idty := identity.FromContext(ctx)
15✔
782
        ctx, err := d.contextWithStorageSettings(ctx)
15✔
783
        if err != nil {
17✔
784
                return err
2✔
785
        }
2✔
786
        // Create an async context that does'nt cancel when server connection
787
        // closes.
788
        ctxAsync := context.Background()
13✔
789
        ctxAsync = log.WithContext(ctxAsync, l)
13✔
790
        ctxAsync = identity.WithContext(ctxAsync, idty)
13✔
791

13✔
792
        settings, _ := storage.SettingsFromContext(ctx)
13✔
793
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
13✔
794
        artifactReader, err := d.objectStorage.GetObject(
13✔
795
                ctxAsync,
13✔
796
                model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
13✔
797
        )
13✔
798
        if err != nil {
17✔
799
                if errors.Is(err, storage.ErrObjectNotFound) {
6✔
800
                        return ErrUploadNotFound
2✔
801
                }
2✔
802
                return err
2✔
803
        }
804

805
        err = d.db.UpdateUploadIntentStatus(
9✔
806
                ctx,
9✔
807
                intentID,
9✔
808
                model.LinkStatusPending,
9✔
809
                model.LinkStatusProcessing,
9✔
810
        )
9✔
811
        if err != nil {
13✔
812
                errClose := artifactReader.Close()
4✔
813
                if errClose != nil {
6✔
814
                        l.Warnf("failed to close artifact reader: %s", errClose)
2✔
815
                }
2✔
816
                if errors.Is(err, store.ErrNotFound) {
6✔
817
                        return ErrUploadNotFound
2✔
818
                }
2✔
819
                return err
2✔
820
        }
821
        go d.processUploadedArtifact( // nolint:errcheck
5✔
822
                ctxAsync, intentID, artifactReader,
5✔
823
        )
5✔
824
        return nil
5✔
825
}
826

827
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
828
        return &model.ArtifactInfo{
1✔
829
                Format:  info.Format,
1✔
830
                Version: uint(info.Version),
1✔
831
        }
1✔
832
}
1✔
833

834
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
1✔
835
        var files []model.UpdateFile
1✔
836
        for _, u := range uFiles {
2✔
837
                files = append(files, model.UpdateFile{
1✔
838
                        Name:     u.Name,
1✔
839
                        Size:     u.Size,
1✔
840
                        Date:     &u.Date,
1✔
841
                        Checksum: string(u.Checksum),
1✔
842
                })
1✔
843
        }
1✔
844
        return files, nil
1✔
845
}
846

847
func getMetaFromArchive(r *io.Reader) (*model.ArtifactMeta, error) {
5✔
848
        metaArtifact := model.NewArtifactMeta()
5✔
849

5✔
850
        aReader := areader.NewReader(*r)
5✔
851

5✔
852
        // There is no signature verification here.
5✔
853
        // It is just simple check if artifact is signed or not.
5✔
854
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
5✔
855
                metaArtifact.Signed = true
×
856
                return nil
×
857
        }
×
858

859
        err := aReader.ReadArtifact()
5✔
860
        if err != nil {
10✔
861
                return nil, errors.Wrap(err, "reading artifact error")
5✔
862
        }
5✔
863

864
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
865
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
866

1✔
867
        metaArtifact.Name = aReader.GetArtifactName()
1✔
868
        if metaArtifact.Info.Version == 3 {
2✔
869
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
870
                if err != nil {
1✔
871
                        return nil, errors.Wrap(err,
×
872
                                "error parsing version 3 artifact")
×
873
                }
×
874

875
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
876
                if err != nil {
1✔
877
                        return nil, errors.Wrap(err,
×
878
                                "error parsing version 3 artifact")
×
879
                }
×
880

881
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
882
        }
883

884
        for _, p := range aReader.GetHandlers() {
2✔
885
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
886
                if err != nil {
1✔
887
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
888
                }
×
889

890
                uMetadata, err := p.GetUpdateMetaData()
1✔
891
                if err != nil {
1✔
892
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
893
                }
×
894

895
                metaArtifact.Updates = append(
1✔
896
                        metaArtifact.Updates,
1✔
897
                        model.Update{
1✔
898
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
899
                                        Type: p.GetUpdateType(),
1✔
900
                                },
1✔
901
                                Files:    uFiles,
1✔
902
                                MetaData: uMetadata,
1✔
903
                        })
1✔
904
        }
905

906
        return metaArtifact, nil
1✔
907
}
908

909
func getArtifactIDs(artifacts []*model.Image) []string {
13✔
910
        artifactIDs := make([]string, 0, len(artifacts))
13✔
911
        for _, artifact := range artifacts {
26✔
912
                artifactIDs = append(artifactIDs, artifact.Id)
13✔
913
        }
13✔
914
        return artifactIDs
13✔
915
}
916

917
// deployments
918
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
8✔
919
        ids := make([]string, len(devices))
8✔
920
        for i, d := range devices {
16✔
921
                ids[i] = d.ID
8✔
922
        }
8✔
923

924
        return ids
8✔
925
}
926

927
// updateDeploymentConstructor fills devices list with device ids
928
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
929
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
10✔
930
        l := log.FromContext(ctx)
10✔
931

10✔
932
        id := identity.FromContext(ctx)
10✔
933
        if id == nil {
10✔
934
                l.Error("identity not present in the context")
×
935
                return nil, ErrModelInternal
×
936
        }
×
937
        searchParams := model.SearchParams{
10✔
938
                Page:    1,
10✔
939
                PerPage: PerPageInventoryDevices,
10✔
940
                Filters: []model.FilterPredicate{
10✔
941
                        {
10✔
942
                                Scope:     InventoryIdentityScope,
10✔
943
                                Attribute: InventoryStatusAttributeName,
10✔
944
                                Type:      "$eq",
10✔
945
                                Value:     InventoryStatusAccepted,
10✔
946
                        },
10✔
947
                },
10✔
948
        }
10✔
949
        if len(constructor.Group) > 0 {
20✔
950
                searchParams.Filters = append(
10✔
951
                        searchParams.Filters,
10✔
952
                        model.FilterPredicate{
10✔
953
                                Scope:     InventoryGroupScope,
10✔
954
                                Attribute: InventoryGroupAttributeName,
10✔
955
                                Type:      "$eq",
10✔
956
                                Value:     constructor.Group,
10✔
957
                        })
10✔
958
        }
10✔
959

960
        for {
22✔
961
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
12✔
962
                if err != nil {
14✔
963
                        l.Errorf("error searching for devices")
2✔
964
                        return nil, ErrModelInternal
2✔
965
                }
2✔
966
                if count < 1 {
12✔
967
                        l.Errorf("no devices found")
2✔
968
                        return nil, ErrNoDevices
2✔
969
                }
2✔
970
                if len(devices) < 1 {
8✔
971
                        break
×
972
                }
973
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
8✔
974
                if len(constructor.Devices) == count {
14✔
975
                        break
6✔
976
                }
977
                searchParams.Page++
2✔
978
        }
979

980
        return constructor, nil
6✔
981
}
982

983
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
984
func (d *Deployments) CreateDeviceConfigurationDeployment(
985
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
986
        deviceID, deploymentID string) (string, error) {
9✔
987

9✔
988
        if constructor == nil {
11✔
989
                return "", ErrModelMissingInput
2✔
990
        }
2✔
991

992
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
7✔
993
                constructor,
7✔
994
                deploymentID,
7✔
995
        )
7✔
996
        if err != nil {
7✔
997
                return "", errors.Wrap(err, "failed to create deployment")
×
998
        }
×
999

1000
        deployment.DeviceList = []string{deviceID}
7✔
1001
        deployment.MaxDevices = 1
7✔
1002
        deployment.Configuration = []byte(constructor.Configuration)
7✔
1003
        deployment.Type = model.DeploymentTypeConfiguration
7✔
1004

7✔
1005
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
7✔
1006
        if err != nil {
9✔
1007
                return "", err
2✔
1008
        }
2✔
1009
        deployment.Groups = groups
5✔
1010

5✔
1011
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
8✔
1012
                if strings.Contains(err.Error(), "duplicate key error") {
4✔
1013
                        return "", ErrDuplicateDeployment
1✔
1014
                }
1✔
1015
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
4✔
1016
                        return "", ErrInvalidDeploymentID
1✔
1017
                }
1✔
1018
                return "", errors.Wrap(err, "Storing deployment data")
2✔
1019
        }
1020

1021
        return deployment.Id, nil
3✔
1022
}
1023

1024
// CreateDeployment precomputes new deployment and schedules it for devices.
1025
func (d *Deployments) CreateDeployment(ctx context.Context,
1026
        constructor *model.DeploymentConstructor) (string, error) {
17✔
1027

17✔
1028
        var err error
17✔
1029

17✔
1030
        if constructor == nil {
19✔
1031
                return "", ErrModelMissingInput
2✔
1032
        }
2✔
1033

1034
        if err := constructor.Validate(); err != nil {
15✔
1035
                return "", errors.Wrap(err, "Validating deployment")
×
1036
        }
×
1037

1038
        if len(constructor.Group) > 0 || constructor.AllDevices {
25✔
1039
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
10✔
1040
                if err != nil {
14✔
1041
                        return "", err
4✔
1042
                }
4✔
1043
        }
1044

1045
        deployment, err := model.NewDeploymentFromConstructor(constructor)
11✔
1046
        if err != nil {
11✔
1047
                return "", errors.Wrap(err, "failed to create deployment")
×
1048
        }
×
1049

1050
        // Assign artifacts to the deployment.
1051
        // When new artifact(s) with the artifact name same as the one in the deployment
1052
        // will be uploaded to the backend, it will also become part of this deployment.
1053
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
11✔
1054
        if err != nil {
11✔
1055
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1056
        }
×
1057

1058
        if len(artifacts) == 0 {
12✔
1059
                return "", ErrNoArtifact
1✔
1060
        }
1✔
1061

1062
        deployment.Artifacts = getArtifactIDs(artifacts)
11✔
1063
        deployment.DeviceList = constructor.Devices
11✔
1064
        deployment.MaxDevices = len(constructor.Devices)
11✔
1065
        deployment.Type = model.DeploymentTypeSoftware
11✔
1066
        if len(constructor.Group) > 0 {
17✔
1067
                deployment.Groups = []string{constructor.Group}
6✔
1068
        }
6✔
1069

1070
        // single device deployment case
1071
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
16✔
1072
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
5✔
1073
                if err != nil {
5✔
1074
                        return "", err
×
1075
                }
×
1076
                deployment.Groups = groups
5✔
1077
        }
1078

1079
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
13✔
1080
                return "", errors.Wrap(err, "Storing deployment data")
2✔
1081
        }
2✔
1082

1083
        return deployment.Id, nil
9✔
1084
}
1085

1086
func (d *Deployments) getDeploymentGroups(
1087
        ctx context.Context,
1088
        devices []string,
1089
) ([]string, error) {
11✔
1090
        id := identity.FromContext(ctx)
11✔
1091

11✔
1092
        //only for single device deployment case
11✔
1093
        if len(devices) != 1 {
11✔
1094
                return nil, nil
×
1095
        }
×
1096

1097
        if id == nil {
12✔
1098
                id = &identity.Identity{}
1✔
1099
        }
1✔
1100

1101
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
11✔
1102
        if err != nil && err != inventory.ErrDevNotFound {
13✔
1103
                return nil, err
2✔
1104
        }
2✔
1105
        return groups, nil
9✔
1106
}
1107

1108
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1109
func (d *Deployments) IsDeploymentFinished(
1110
        ctx context.Context,
1111
        deploymentID string,
1112
) (bool, error) {
1✔
1113
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
1114
        if err != nil {
1✔
1115
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1116
        }
×
1117
        if deployment == nil {
2✔
1118
                return true, nil
1✔
1119
        }
1✔
1120

1121
        return false, nil
1✔
1122
}
1123

1124
// GetDeployment fetches deployment by ID
1125
func (d *Deployments) GetDeployment(ctx context.Context,
1126
        deploymentID string) (*model.Deployment, error) {
1✔
1127

1✔
1128
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1129
        if err != nil {
1✔
1130
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1131
        }
×
1132

1133
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1134
                return nil, err
×
1135
        }
×
1136

1137
        return deployment, nil
1✔
1138
}
1139

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

7✔
1145
        var found bool
7✔
1146

7✔
1147
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
7✔
1148
        if err != nil {
9✔
1149
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
2✔
1150
        }
2✔
1151

1152
        if found {
6✔
1153
                return found, nil
1✔
1154
        }
1✔
1155

1156
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx,
5✔
1157
                imageID, model.ActiveDeploymentStatuses()...)
5✔
1158
        if err != nil {
7✔
1159
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
2✔
1160
        }
2✔
1161

1162
        return found, nil
3✔
1163
}
1164

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

×
1169
        var found bool
×
1170

×
1171
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
×
1172
        if err != nil {
×
1173
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1174
        }
×
1175

1176
        if found {
×
1177
                return found, nil
×
1178
        }
×
1179

1180
        found, err = d.db.ExistAssignedImageWithIDAndStatuses(ctx, imageID)
×
1181
        if err != nil {
×
1182
                return false, errors.Wrap(err, "Checking if image is used in deployment")
×
1183
        }
×
1184

1185
        return found, nil
×
1186
}
1187

1188
// Retrieves the model.Deployment and model.DeviceDeployment structures
1189
// for the device. Upon error, nil is returned for both deployment structures.
1190
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1191
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
3✔
1192

3✔
1193
        // Retrieve device deployment
3✔
1194
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
3✔
1195

3✔
1196
        if err != nil {
3✔
1197
                return nil, nil, errors.Wrap(err,
×
1198
                        "Searching for oldest active deployment for the device")
×
1199
        } else if deviceDeployment == nil {
4✔
1200
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
1201
        }
1✔
1202

1203
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
3✔
1204
        if err != nil {
3✔
1205
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1206
        }
×
1207
        if deployment == nil {
3✔
1208
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1209
        }
×
1210

1211
        return deployment, deviceDeployment, nil
3✔
1212
}
1213

1214
// getNewDeploymentForDevice returns deployment object and creates and returns
1215
// new device deployment for the device;
1216
//
1217
// we are interested only in the deployments that are newer than the latest
1218
// deployment applied by the device;
1219
// this way we guarantee that the device will not receive deployment
1220
// that is older than the one installed on the device;
1221
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
1222
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1223

1✔
1224
        var lastDeployment *time.Time
1✔
1225
        //get latest device deployment for the device;
1✔
1226
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
1227
        if err != nil {
1✔
1228
                return nil, nil, errors.Wrap(err,
×
1229
                        "Searching for latest active deployment for the device")
×
1230
        } else if deviceDeployment == nil {
2✔
1231
                lastDeployment = &time.Time{}
1✔
1232
        } else {
2✔
1233
                lastDeployment = deviceDeployment.Created
1✔
1234
        }
1✔
1235

1236
        //get deployments newer then last device deployment
1237
        //iterate over deployments and check if the device is part of the deployment or not
1238
        for skip := 0; true; skip += 100 {
2✔
1239
                deployments, err := d.db.FindNewerActiveDeployments(ctx, lastDeployment, skip, 100)
1✔
1240
                if err != nil {
1✔
1241
                        return nil, nil, errors.Wrap(err,
×
1242
                                "Failed to search for newer active deployments")
×
1243
                }
×
1244
                if len(deployments) == 0 {
2✔
1245
                        return nil, nil, nil
1✔
1246
                }
1✔
1247

1248
                for _, deployment := range deployments {
2✔
1249
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceID, deployment)
1✔
1250
                        if err != nil {
1✔
1251
                                return nil, nil, err
×
1252
                        }
×
1253
                        if ok {
2✔
1254
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
1255
                                        deviceID, deployment, model.DeviceDeploymentStatusPending)
1✔
1256
                                if err != nil {
1✔
1257
                                        return nil, nil, err
×
1258
                                }
×
1259
                                return deployment, deviceDeployment, nil
1✔
1260
                        }
1261
                }
1262
        }
1263

1264
        return nil, nil, nil
×
1265
}
1266

1267
func (d *Deployments) createDeviceDeploymentWithStatus(
1268
        ctx context.Context, deviceID string,
1269
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1270
) (*model.DeviceDeployment, error) {
11✔
1271
        prevStatus := model.DeviceDeploymentStatusNull
11✔
1272
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
11✔
1273
        if err != nil && err != mongo.ErrStorageNotFound {
11✔
1274
                return nil, err
×
1275
        } else if deviceDeployment != nil {
11✔
1276
                prevStatus = deviceDeployment.Status
×
1277
        }
×
1278

1279
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
11✔
1280
        deviceDeployment.Status = status
11✔
1281
        deviceDeployment.Active = status.Active()
11✔
1282
        deviceDeployment.Created = deployment.Created
11✔
1283

11✔
1284
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
11✔
1285
                return nil, err
×
1286
        }
×
1287

1288
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
11✔
1289
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
11✔
1290
                return nil, err
×
1291
        }
×
1292

1293
        // after inserting new device deployment update deployment stats
1294
        // in the database and locally, and update deployment status
1295
        if err := d.db.UpdateStatsInc(
11✔
1296
                ctx, deployment.Id,
11✔
1297
                prevStatus, status,
11✔
1298
        ); err != nil {
11✔
1299
                return nil, err
×
1300
        }
×
1301

1302
        deployment.Stats.Inc(status)
11✔
1303

11✔
1304
        err = d.recalcDeploymentStatus(ctx, deployment)
11✔
1305
        if err != nil {
11✔
1306
                return nil, errors.Wrap(err, "failed to update deployment status")
×
1307
        }
×
1308

1309
        if !status.Active() {
21✔
1310
                err := d.reindexDevice(ctx, deviceID)
10✔
1311
                if err != nil {
10✔
1312
                        l := log.FromContext(ctx)
×
1313
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1314
                }
×
1315
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
10✔
1316
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
10✔
1317
                        l := log.FromContext(ctx)
×
1318
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1319
                }
×
1320
        }
1321

1322
        return deviceDeployment, nil
11✔
1323
}
1324

1325
func (d *Deployments) isDevicePartOfDeployment(
1326
        ctx context.Context,
1327
        deviceID string,
1328
        deployment *model.Deployment,
1329
) (bool, error) {
15✔
1330
        for _, id := range deployment.DeviceList {
26✔
1331
                if id == deviceID {
22✔
1332
                        return true, nil
11✔
1333
                }
11✔
1334
        }
1335
        return false, nil
5✔
1336
}
1337

1338
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1339
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1340
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
3✔
1341

3✔
1342
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
3✔
1343
        if err != nil {
3✔
1344
                return nil, ErrModelInternal
×
1345
        } else if deployment == nil {
4✔
1346
                return nil, nil
1✔
1347
        }
1✔
1348

1349
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
3✔
1350
        if err != nil {
4✔
1351
                return nil, err
1✔
1352
        }
1✔
1353
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
3✔
1354
}
1355

1356
func (d *Deployments) getDeploymentInstructions(
1357
        ctx context.Context,
1358
        deployment *model.Deployment,
1359
        deviceDeployment *model.DeviceDeployment,
1360
        request *model.DeploymentNextRequest,
1361
) (*model.DeploymentInstructions, error) {
3✔
1362

3✔
1363
        var newArtifactAssigned bool
3✔
1364

3✔
1365
        l := log.FromContext(ctx)
3✔
1366

3✔
1367
        if deployment.Type == model.DeploymentTypeConfiguration {
4✔
1368
                // There's nothing more we need to do, the link must be filled
1✔
1369
                // in by the API layer.
1✔
1370
                return &model.DeploymentInstructions{
1✔
1371
                        ID: deployment.Id,
1✔
1372
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
1373
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
1374
                                // use deployment ID togheter with device ID as artifact ID
1✔
1375
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
1376
                                ArtifactName:          deployment.ArtifactName,
1✔
1377
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
1378
                        },
1✔
1379
                        Type: model.DeploymentTypeConfiguration,
1✔
1380
                }, nil
1✔
1381
        }
1✔
1382

1383
        // assing artifact to the device deployment
1384
        // only if it was not assgined previously
1385
        if deviceDeployment.Image == nil {
6✔
1386
                if err := d.assignArtifact(
3✔
1387
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
3✔
1388
                        return nil, err
×
1389
                }
×
1390
                newArtifactAssigned = true
3✔
1391
        }
1392

1393
        if deviceDeployment.Image == nil {
3✔
1394
                // No artifact - return empty response
×
1395
                return nil, nil
×
1396
        }
×
1397

1398
        // if the deployment is not forcing the installation, and
1399
        // if artifact was recognized as already installed, and this is
1400
        // a new device deployment - indicated by device deployment status "pending",
1401
        // handle already installed artifact case
1402
        if !deployment.ForceInstallation &&
3✔
1403
                d.isAlreadyInstalled(request, deviceDeployment) &&
3✔
1404
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
6✔
1405
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
3✔
1406
        }
3✔
1407

1408
        // if new artifact has been assigned to device deployment
1409
        // add artifact size to deployment total size,
1410
        // before returning deployment instruction to the device
1411
        if newArtifactAssigned {
2✔
1412
                if err := d.db.IncrementDeploymentTotalSize(
1✔
1413
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
1414
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1415
                }
×
1416
        }
1417

1418
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
1419
        if err != nil {
1✔
1420
                return nil, err
×
1421
        }
×
1422

1423
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
1424
        link, err := d.objectStorage.GetRequest(
1✔
1425
                ctx,
1✔
1426
                imagePath,
1✔
1427
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
1428
                DefaultUpdateDownloadLinkExpire,
1✔
1429
        )
1✔
1430
        if err != nil {
1✔
1431
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1432
        }
×
1433

1434
        instructions := &model.DeploymentInstructions{
1✔
1435
                ID: deviceDeployment.DeploymentId,
1✔
1436
                Artifact: model.ArtifactDeploymentInstructions{
1✔
1437
                        ID: deviceDeployment.Image.Id,
1✔
1438
                        ArtifactName: deviceDeployment.Image.
1✔
1439
                                ArtifactMeta.Name,
1✔
1440
                        Source: *link,
1✔
1441
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
1442
                                ArtifactMeta.DeviceTypesCompatible,
1✔
1443
                },
1✔
1444
        }
1✔
1445

1✔
1446
        return instructions, nil
1✔
1447
}
1448

1449
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1450
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
3✔
1451
        if deviceDeployment.Request != nil {
4✔
1452
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
1453
                        // the device reported different device type and/or artifact name
1✔
1454
                        // during the update process, which should never happen;
1✔
1455
                        // mark deployment for this device as failed to force client to rollback
1✔
1456
                        l := log.FromContext(ctx)
1✔
1457
                        l.Errorf(
1✔
1458
                                "Device with id %s reported new data: %s during update process;"+
1✔
1459
                                        "old data: %s",
1✔
1460
                                deviceID, request, deviceDeployment.Request)
1✔
1461

1✔
1462
                        if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId, deviceID,
1✔
1463
                                model.DeviceDeploymentState{
1✔
1464
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
1465
                                }); err != nil {
1✔
1466
                                return errors.Wrap(err, "Failed to update deployment status")
×
1467
                        }
×
1468
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1469
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1470
                        }
×
1471
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1472
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1473
                                l := log.FromContext(ctx)
×
1474
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1475
                        }
×
1476
                        return ErrConflictingRequestData
1✔
1477
                }
1478
        } else {
3✔
1479
                // save the request
3✔
1480
                if err := d.db.SaveDeviceDeploymentRequest(
3✔
1481
                        ctx, deviceDeployment.Id, request); err != nil {
3✔
1482
                        return err
×
1483
                }
×
1484
        }
1485
        return nil
3✔
1486
}
1487

1488
// UpdateDeviceDeploymentStatus will update the deployment status for device of
1489
// ID `deviceID`. Returns nil if update was successful.
1490
func (d *Deployments) UpdateDeviceDeploymentStatus(ctx context.Context, deploymentID string,
1491
        deviceID string, ddState model.DeviceDeploymentState) error {
11✔
1492

11✔
1493
        l := log.FromContext(ctx)
11✔
1494

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

11✔
1497
        var finishTime *time.Time = nil
11✔
1498
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
18✔
1499
                now := time.Now()
7✔
1500
                finishTime = &now
7✔
1501
        }
7✔
1502

1503
        dd, err := d.db.GetDeviceDeployment(ctx, deploymentID, deviceID, false)
11✔
1504
        if err == mongo.ErrStorageNotFound {
13✔
1505
                return ErrStorageNotFound
2✔
1506
        } else if err != nil {
11✔
1507
                return err
×
1508
        }
×
1509

1510
        currentStatus := dd.Status
9✔
1511

9✔
1512
        if currentStatus == model.DeviceDeploymentStatusAborted {
9✔
1513
                return ErrDeploymentAborted
×
1514
        }
×
1515

1516
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
9✔
1517
                return ErrDeviceDecommissioned
×
1518
        }
×
1519

1520
        // nothing to do
1521
        if ddState.Status == currentStatus {
9✔
1522
                return nil
×
1523
        }
×
1524

1525
        // update finish time
1526
        ddState.FinishTime = finishTime
9✔
1527

9✔
1528
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
9✔
1529
                deviceID, deploymentID, ddState)
9✔
1530
        if err != nil {
9✔
1531
                return err
×
1532
        }
×
1533

1534
        if err = d.db.UpdateStatsInc(ctx, deploymentID, old, ddState.Status); err != nil {
9✔
1535
                return err
×
1536
        }
×
1537

1538
        // fetch deployment stats and update deployment status
1539
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
9✔
1540
        if err != nil {
9✔
1541
                return errors.Wrap(err, "failed when searching for deployment")
×
1542
        }
×
1543

1544
        err = d.recalcDeploymentStatus(ctx, deployment)
9✔
1545
        if err != nil {
9✔
1546
                return errors.Wrap(err, "failed to update deployment status")
×
1547
        }
×
1548

1549
        if !ddState.Status.Active() {
16✔
1550
                l := log.FromContext(ctx)
7✔
1551
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, *dd); err != nil {
7✔
1552
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1553
                }
×
1554
                if err := d.reindexDevice(ctx, deviceID); err != nil {
7✔
1555
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1556
                }
×
1557
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
7✔
1558
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1559
                }
×
1560
        }
1561

1562
        return nil
9✔
1563
}
1564

1565
// recalcDeploymentStatus inspects the deployment stats and
1566
// recalculates and updates its status
1567
// it should be used whenever deployment stats are touched
1568
func (d *Deployments) recalcDeploymentStatus(ctx context.Context, dep *model.Deployment) error {
19✔
1569
        status := dep.GetStatus()
19✔
1570

19✔
1571
        if err := d.db.SetDeploymentStatus(ctx, dep.Id, status, time.Now()); err != nil {
19✔
1572
                return err
×
1573
        }
×
1574

1575
        return nil
19✔
1576
}
1577

1578
func (d *Deployments) GetDeploymentStats(ctx context.Context,
1579
        deploymentID string) (model.Stats, error) {
1✔
1580

1✔
1581
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1582

1✔
1583
        if err != nil {
1✔
1584
                return nil, errors.Wrap(err, "checking deployment id")
×
1585
        }
×
1586

1587
        if deployment == nil {
1✔
1588
                return nil, nil
×
1589
        }
×
1590

1591
        return deployment.Stats, nil
1✔
1592
}
1593
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1594
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1595

×
1596
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1597

×
1598
        if err != nil {
×
1599
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1600
        }
×
1601

1602
        if deploymentStats == nil {
×
1603
                return nil, ErrModelDeploymentNotFound
×
1604
        }
×
1605

1606
        return deploymentStats, nil
×
1607
}
1608

1609
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1610
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
1611
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
1612

1✔
1613
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
1614
        if err != nil {
1✔
1615
                return nil, ErrModelInternal
×
1616
        }
×
1617

1618
        if deployment == nil {
1✔
1619
                return nil, ErrModelDeploymentNotFound
×
1620
        }
×
1621

1622
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
1623
        if err != nil {
1✔
1624
                return nil, ErrModelInternal
×
1625
        }
×
1626

1627
        return statuses, nil
1✔
1628
}
1629

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

1✔
1633
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
1634
        if err != nil {
1✔
1635
                return nil, -1, ErrModelInternal
×
1636
        }
×
1637

1638
        if deployment == nil {
1✔
1639
                return nil, -1, ErrModelDeploymentNotFound
×
1640
        }
×
1641

1642
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
1643
        if err != nil {
1✔
1644
                return nil, -1, ErrModelInternal
×
1645
        }
×
1646

1647
        return statuses, totalCount, nil
1✔
1648
}
1649

1650
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1651
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
8✔
1652
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
8✔
1653
        if err != nil {
10✔
1654
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
2✔
1655
        }
2✔
1656

1657
        deploymentIDs := make([]string, len(deviceDeployments))
6✔
1658
        for i, deviceDeployment := range deviceDeployments {
18✔
1659
                deploymentIDs[i] = deviceDeployment.DeploymentId
12✔
1660
        }
12✔
1661

1662
        deployments, _, err := d.db.Find(ctx, model.Query{
6✔
1663
                IDs:          deploymentIDs,
6✔
1664
                Limit:        len(deviceDeployments),
6✔
1665
                DisableCount: true,
6✔
1666
        })
6✔
1667
        if err != nil {
8✔
1668
                return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
2✔
1669
        }
2✔
1670

1671
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
4✔
1672
        for _, deployment := range deployments {
10✔
1673
                deploymentsMap[deployment.Id] = deployment
6✔
1674
        }
6✔
1675

1676
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
4✔
1677
        for i, deviceDeployment := range deviceDeployments {
12✔
1678
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
14✔
1679
                        res = append(res, model.DeviceDeploymentListItem{
6✔
1680
                                Id:         deviceDeployment.Id,
6✔
1681
                                Deployment: deployment,
6✔
1682
                                Device:     &deviceDeployments[i],
6✔
1683
                        })
6✔
1684
                } else {
8✔
1685
                        res = append(res, model.DeviceDeploymentListItem{
2✔
1686
                                Id:     deviceDeployment.Id,
2✔
1687
                                Device: &deviceDeployments[i],
2✔
1688
                        })
2✔
1689
                }
2✔
1690
        }
1691

1692
        return res, totalCount, nil
4✔
1693
}
1694

1695
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1696
        ctx context.Context,
1697
        deployment *model.Deployment,
1698
) error {
11✔
1699
        if deployment.DeviceCount == nil {
11✔
1700
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1701
                if err != nil {
×
1702
                        return errors.Wrap(err, "counting device deployments")
×
1703
                }
×
1704
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1705
                if err != nil {
×
1706
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1707
                }
×
1708
                deployment.DeviceCount = &deviceCount
×
1709
        }
1710

1711
        return nil
11✔
1712
}
1713

1714
func (d *Deployments) LookupDeployment(ctx context.Context,
1715
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1716
        list, totalCount, err := d.db.Find(ctx, query)
1✔
1717

1✔
1718
        if err != nil {
1✔
1719
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1720
        }
×
1721

1722
        if list == nil {
2✔
1723
                return make([]*model.Deployment, 0), 0, nil
1✔
1724
        }
1✔
1725

1726
        for _, deployment := range list {
×
1727
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1728
                        return nil, 0, err
×
1729
                }
×
1730
        }
1731

1732
        return list, totalCount, nil
×
1733
}
1734

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

1✔
1740
        // repack to temporary deployment log and validate
1✔
1741
        dlog := model.DeploymentLog{
1✔
1742
                DeviceID:     deviceID,
1✔
1743
                DeploymentID: deploymentID,
1✔
1744
                Messages:     logs,
1✔
1745
        }
1✔
1746
        if err := dlog.Validate(); err != nil {
1✔
1747
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1748
        }
×
1749

1750
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1751
                if err != nil {
×
1752
                        return err
×
1753
                } else {
×
1754
                        return ErrModelDeploymentNotFound
×
1755
                }
×
1756
        }
1757

1758
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1759
                return err
×
1760
        }
×
1761

1762
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
1763
                deviceID, deploymentID, true)
1✔
1764
}
1765

1766
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
1767
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
1768

1✔
1769
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
1770
                deviceID, deploymentID)
1✔
1771
}
1✔
1772

1773
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
1774
        deploymentID string, deviceID string) (bool, error) {
1✔
1775
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
1776
}
1✔
1777

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

9✔
1781
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
11✔
1782
                return err
2✔
1783
        }
2✔
1784

1785
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
7✔
1786
                ctx, deploymentID)
7✔
1787
        if err != nil {
9✔
1788
                return err
2✔
1789
        }
2✔
1790

1791
        // update statistics
1792
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
7✔
1793
                return errors.Wrap(err, "failed to update deployment stats")
2✔
1794
        }
2✔
1795

1796
        // when aborting the deployment we need to set status directly instead of
1797
        // using recalcDeploymentStatus method;
1798
        // it is possible that the deployment does not have any device deployments yet;
1799
        // in that case, all statistics are 0 and calculating status based on statistics
1800
        // will not work - the calculated status will be "pending"
1801
        if err := d.db.SetDeploymentStatus(ctx,
3✔
1802
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
3✔
1803
                return errors.Wrap(err, "failed to update deployment status")
×
1804
        }
×
1805

1806
        return nil
3✔
1807
}
1808

1809
func (d *Deployments) updateDeviceDeploymentsStatus(
1810
        ctx context.Context,
1811
        deviceId string,
1812
        status model.DeviceDeploymentStatus,
1813
) error {
30✔
1814
        var latestDeployment *time.Time
30✔
1815
        // Retrieve active device deployment for the device
30✔
1816
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
30✔
1817
        if err != nil {
34✔
1818
                return errors.Wrap(err, "Searching for active deployment for the device")
4✔
1819
        } else if deviceDeployment != nil {
34✔
1820
                now := time.Now()
4✔
1821
                ddStatus := model.DeviceDeploymentState{
4✔
1822
                        Status:     status,
4✔
1823
                        FinishTime: &now,
4✔
1824
                }
4✔
1825
                if err := d.UpdateDeviceDeploymentStatus(ctx, deviceDeployment.DeploymentId,
4✔
1826
                        deviceId, ddStatus); err != nil {
4✔
1827
                        return errors.Wrap(err, "updating device deployment status")
×
1828
                }
×
1829
                latestDeployment = deviceDeployment.Created
4✔
1830
        } else {
22✔
1831
                // get latest device deployment for the device
22✔
1832
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
22✔
1833
                if err != nil {
22✔
1834
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1835
                } else if deviceDeployment == nil {
40✔
1836
                        latestDeployment = &time.Time{}
18✔
1837
                } else {
22✔
1838
                        latestDeployment = deviceDeployment.Created
4✔
1839
                }
4✔
1840
        }
1841

1842
        // get deployments newer then last device deployment
1843
        // iterate over deployments and check if the device is part of the deployment or not
1844
        // if the device is part of the deployment create new, decommisioned device deployment
1845
        for skip := 0; true; skip += 100 {
66✔
1846
                deployments, err := d.db.FindNewerActiveDeployments(ctx, latestDeployment, skip, 100)
40✔
1847
                if err != nil {
40✔
1848
                        return errors.Wrap(err, "Failed to search for newer active deployments")
×
1849
                }
×
1850
                if len(deployments) == 0 {
66✔
1851
                        break
26✔
1852
                }
1853
                for _, deployment := range deployments {
28✔
1854
                        ok, err := d.isDevicePartOfDeployment(ctx, deviceId, deployment)
14✔
1855
                        if err != nil {
14✔
1856
                                return err
×
1857
                        }
×
1858
                        if ok {
24✔
1859
                                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
10✔
1860
                                        deviceId, deployment, status)
10✔
1861
                                if err != nil {
10✔
1862
                                        return err
×
1863
                                }
×
1864
                                if !status.Active() {
20✔
1865
                                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
10✔
1866
                                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
10✔
1867
                                                l := log.FromContext(ctx)
×
1868
                                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1869
                                        }
×
1870
                                }
1871
                        }
1872
                }
1873
        }
1874

1875
        if err := d.reindexDevice(ctx, deviceId); err != nil {
26✔
1876
                l := log.FromContext(ctx)
×
1877
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1878
        }
×
1879

1880
        return nil
26✔
1881
}
1882

1883
// DecommissionDevice updates the status of all the pending and active deployments for a device
1884
// to decommissioned
1885
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
14✔
1886
        return d.updateDeviceDeploymentsStatus(
14✔
1887
                ctx,
14✔
1888
                deviceId,
14✔
1889
                model.DeviceDeploymentStatusDecommissioned,
14✔
1890
        )
14✔
1891
}
14✔
1892

1893
// AbortDeviceDeployments aborts all the pending and active deployments for a device
1894
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
16✔
1895
        return d.updateDeviceDeploymentsStatus(
16✔
1896
                ctx,
16✔
1897
                deviceId,
16✔
1898
                model.DeviceDeploymentStatusAborted,
16✔
1899
        )
16✔
1900
}
16✔
1901

1902
// DeleteDeviceDeploymentsHistory deletes the device deployments history
1903
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
4✔
1904
        // get device deployments which will be marked as deleted
4✔
1905
        f := false
4✔
1906
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
4✔
1907
        if err != nil {
4✔
1908
                return err
×
1909
        }
×
1910

1911
        // no device deployments to update
1912
        if len(dd) <= 0 {
4✔
1913
                return nil
×
1914
        }
×
1915

1916
        // mark device deployments as deleted
1917
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
6✔
1918
                return err
2✔
1919
        }
2✔
1920

1921
        // trigger reindexing of updated device deployments
1922
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
2✔
1923
        for i, d := range dd {
4✔
1924
                deviceDeployments[i].ID = d.Id
2✔
1925
                deviceDeployments[i].DeviceID = d.DeviceId
2✔
1926
                deviceDeployments[i].DeploymentID = d.DeploymentId
2✔
1927
        }
2✔
1928
        return d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
2✔
1929
}
1930

1931
// Storage settings
1932
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
5✔
1933
        settings, err := d.db.GetStorageSettings(ctx)
5✔
1934
        if err != nil {
7✔
1935
                return nil, errors.Wrap(err, "Searching for settings failed")
2✔
1936
        }
2✔
1937

1938
        return settings, nil
3✔
1939
}
1940

1941
func (d *Deployments) SetStorageSettings(
1942
        ctx context.Context,
1943
        storageSettings *model.StorageSettings,
1944
) error {
7✔
1945
        if storageSettings != nil {
14✔
1946
                ctx = storage.SettingsWithContext(ctx, storageSettings)
7✔
1947
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
7✔
1948
                        return errors.WithMessage(err,
×
1949
                                "the provided storage settings failed the health check",
×
1950
                        )
×
1951
                }
×
1952
        }
1953
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
11✔
1954
                return errors.Wrap(err, "Failed to save settings")
4✔
1955
        }
4✔
1956

1957
        return nil
3✔
1958
}
1959

1960
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
15✔
1961
        d.reportingClient = c
15✔
1962
        return d
15✔
1963
}
15✔
1964

1965
func (d *Deployments) haveReporting() bool {
12✔
1966
        return d.reportingClient != nil
12✔
1967
}
12✔
1968

1969
func (d *Deployments) search(
1970
        ctx context.Context,
1971
        tid string,
1972
        parms model.SearchParams,
1973
) ([]model.InvDevice, int, error) {
12✔
1974
        if d.haveReporting() {
14✔
1975
                return d.reportingClient.Search(ctx, tid, parms)
2✔
1976
        } else {
12✔
1977
                return d.inventoryClient.Search(ctx, tid, parms)
10✔
1978
        }
10✔
1979
}
1980

1981
func (d *Deployments) UpdateDeploymentsWithArtifactName(
1982
        ctx context.Context,
1983
        artifactName string,
1984
) error {
3✔
1985
        // first check if there are pending deployments with given artifact name
3✔
1986
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
3✔
1987
        if err != nil {
3✔
1988
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
1989
        }
×
1990
        if !exists {
4✔
1991
                return nil
1✔
1992
        }
1✔
1993

1994
        // Assign artifacts to the deployments with given artifact name
1995
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
2✔
1996
        if err != nil {
2✔
1997
                return errors.Wrap(err, "Finding artifact with given name")
×
1998
        }
×
1999

2000
        if len(artifacts) == 0 {
2✔
2001
                return ErrNoArtifact
×
2002
        }
×
2003
        artifactIDs := getArtifactIDs(artifacts)
2✔
2004
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
2✔
2005
}
2006

2007
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
49✔
2008
        if d.reportingClient != nil {
54✔
2009
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
5✔
2010
        }
5✔
2011
        return nil
44✔
2012
}
2013

2014
func (d *Deployments) reindexDeployment(ctx context.Context,
2015
        deviceID, deploymentID, ID string) error {
33✔
2016
        if d.reportingClient != nil {
38✔
2017
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
5✔
2018
        }
5✔
2019
        return nil
28✔
2020
}
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