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

mendersoftware / mender-server / 1622978334

13 Jan 2025 03:51PM UTC coverage: 72.802% (-3.8%) from 76.608%
1622978334

Pull #300

gitlab-ci

alfrunes
fix: Deployment device count should not exceed max devices

Added a condition to skip deployments when the device count reaches max
devices.

Changelog: Title
Ticket: MEN-7847
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #300: fix: Deployment device count should not exceed max devices

4251 of 6164 branches covered (68.96%)

Branch coverage included in aggregate %.

0 of 18 new or added lines in 1 file covered. (0.0%)

2544 existing lines in 83 files now uncovered.

42741 of 58384 relevant lines covered (73.21%)

21.49 hits per line

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

56.74
/backend/services/deployments/app/app.go
1
// Copyright 2024 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/mender-artifact/areader"
31
        "github.com/mendersoftware/mender-artifact/artifact"
32
        "github.com/mendersoftware/mender-artifact/awriter"
33
        "github.com/mendersoftware/mender-artifact/handlers"
34

35
        "github.com/mendersoftware/mender-server/pkg/identity"
36
        "github.com/mendersoftware/mender-server/pkg/log"
37

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

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

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

63
        fileSuffixTmp = ".tmp"
64

65
        inprogressIdleTime = time.Hour
66
)
67

68
var (
69
        ArtifactConfigureType = "mender-configure"
70
)
71

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

89
        ErrMsgArtifactConflict = "An artifact with the same name has conflicting dependencies"
90

91
        // deployments
92
        ErrModelMissingInput       = errors.New("Missing input deployment data")
93
        ErrModelInvalidDeviceID    = errors.New("Invalid device ID")
94
        ErrModelDeploymentNotFound = errors.New("Deployment not found")
95
        ErrModelInternal           = errors.New("Internal error")
96
        ErrStorageInvalidLog       = errors.New("Invalid deployment log")
97
        ErrStorageNotFound         = errors.New("Not found")
98
        ErrDeploymentAborted       = errors.New("Deployment aborted")
99
        ErrDeviceDecommissioned    = errors.New("Device decommissioned")
100
        ErrNoArtifact              = errors.New("No artifact for the deployment")
101
        ErrNoDevices               = errors.New("No devices for the deployment")
102
        ErrDuplicateDeployment     = errors.New("Deployment with given ID already exists")
103
        ErrInvalidDeploymentID     = errors.New("Deployment ID must be a valid UUID")
104
        ErrConflictingRequestData  = errors.New("Device provided conflicting request data")
105
        ErrConflictingDeployment   = errors.New(
106
                "Invalid deployment definition: there is already an active deployment with " +
107
                        "the same parameters",
108
        )
109
)
110

111
//deployments
112

113
//go:generate ../../../utils/mockgen.sh
114
type App interface {
115
        HealthCheck(ctx context.Context) error
116
        // limits
117
        GetLimit(ctx context.Context, name string) (*model.Limit, error)
118
        ProvisionTenant(ctx context.Context, tenant_id string) error
119

120
        // Storage Settings
121
        GetStorageSettings(ctx context.Context) (*model.StorageSettings, error)
122
        SetStorageSettings(ctx context.Context, storageSettings *model.StorageSettings) error
123

124
        // images
125
        ListImages(
126
                ctx context.Context,
127
                filters *model.ReleaseOrImageFilter,
128
        ) ([]*model.Image, int, error)
129
        DownloadLink(ctx context.Context, imageID string,
130
                expire time.Duration) (*model.Link, error)
131
        UploadLink(
132
                ctx context.Context,
133
                expire time.Duration,
134
                skipVerify bool,
135
        ) (*model.UploadLink, error)
136
        CompleteUpload(
137
                ctx context.Context,
138
                intentID string,
139
                skipVerify bool,
140
                metadata *model.DirectUploadMetadata,
141
        ) error
142
        GetImage(ctx context.Context, id string) (*model.Image, error)
143
        DeleteImage(ctx context.Context, imageID string) error
144
        CreateImage(ctx context.Context,
145
                multipartUploadMsg *model.MultipartUploadMsg) (string, error)
146
        GenerateImage(ctx context.Context,
147
                multipartUploadMsg *model.MultipartGenerateImageMsg) (string, error)
148
        GenerateConfigurationImage(
149
                ctx context.Context,
150
                deviceType string,
151
                deploymentID string,
152
        ) (io.Reader, error)
153
        EditImage(ctx context.Context, id string,
154
                constructorData *model.ImageMeta) (bool, error)
155

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

201
        // releases
202
        ReplaceReleaseTags(ctx context.Context, releaseName string, tags model.Tags) error
203
        UpdateRelease(ctx context.Context, releaseName string, release model.ReleasePatch) error
204
        ListReleaseTags(ctx context.Context) (model.Tags, error)
205
        GetReleasesUpdateTypes(ctx context.Context) ([]string, error)
206
        DeleteReleases(ctx context.Context, releaseNames []string) ([]string, error)
207
        GetRelease(ctx context.Context, releaseName string) (*model.Release, error)
208
}
209

210
type Deployments struct {
211
        db              store.DataStore
212
        objectStorage   storage.ObjectStorage
213
        workflowsClient workflows.Client
214
        inventoryClient inventory.Client
215
        reportingClient reporting.Client
216
}
217

218
// Compile-time check
219
var _ App = &Deployments{}
220

221
func NewDeployments(
222
        storage store.DataStore,
223
        objectStorage storage.ObjectStorage,
224
        maxActiveDeployments int64,
225
        withAuditLogs bool,
226
) *Deployments {
1✔
227
        return &Deployments{
1✔
228
                db:              storage,
1✔
229
                objectStorage:   objectStorage,
1✔
230
                workflowsClient: workflows.NewClient(),
1✔
231
                inventoryClient: inventory.NewClient(),
1✔
232
        }
1✔
233
}
1✔
234

235
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
1✔
236
        d.workflowsClient = workflowsClient
1✔
237
}
1✔
238

239
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
1✔
240
        d.inventoryClient = inventoryClient
1✔
241
}
1✔
242

243
func (d *Deployments) HealthCheck(ctx context.Context) error {
1✔
244
        err := d.db.Ping(ctx)
1✔
245
        if err != nil {
2✔
246
                return errors.Wrap(err, "error reaching MongoDB")
1✔
247
        }
1✔
248
        err = d.objectStorage.HealthCheck(ctx)
1✔
249
        if err != nil {
2✔
250
                return errors.Wrap(
1✔
251
                        err,
1✔
252
                        "error reaching artifact storage service",
1✔
253
                )
1✔
254
        }
1✔
255

256
        err = d.workflowsClient.CheckHealth(ctx)
1✔
257
        if err != nil {
2✔
258
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
259
        }
1✔
260

261
        err = d.inventoryClient.CheckHealth(ctx)
1✔
262
        if err != nil {
2✔
263
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
264
        }
1✔
265

266
        if d.reportingClient != nil {
2✔
267
                err = d.reportingClient.CheckHealth(ctx)
1✔
268
                if err != nil {
2✔
269
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
270
                }
1✔
271
        }
272
        return nil
1✔
273
}
274

275
func (d *Deployments) contextWithStorageSettings(
276
        ctx context.Context,
277
) (context.Context, error) {
1✔
278
        var err error
1✔
279
        settings, ok := storage.SettingsFromContext(ctx)
1✔
280
        if !ok {
2✔
281
                settings, err = d.db.GetStorageSettings(ctx)
1✔
282
                if err != nil {
2✔
283
                        return nil, err
1✔
284
                }
1✔
285
        }
286
        if settings != nil {
1✔
287
                if settings.UseAccelerate && settings.Uri != "" {
×
288
                        log.FromContext(ctx).
×
289
                                Warn(`storage settings: custom "uri" and "use_accelerate" ` +
×
290
                                        `are not allowed: disabling transfer acceleration`)
×
291
                        settings.UseAccelerate = false
×
292
                }
×
293
                err = settings.Validate()
×
294
                if err != nil {
×
295
                        return nil, err
×
296
                }
×
297
        }
298
        return storage.SettingsWithContext(ctx, settings), nil
1✔
299
}
300

301
func (d *Deployments) GetLimit(ctx context.Context, name string) (*model.Limit, error) {
1✔
302
        limit, err := d.db.GetLimit(ctx, name)
1✔
303
        if err == mongo.ErrLimitNotFound {
2✔
304
                return &model.Limit{
1✔
305
                        Name:  name,
1✔
306
                        Value: 0,
1✔
307
                }, nil
1✔
308

1✔
309
        } else if err != nil {
3✔
310
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
311
        }
1✔
312
        return limit, nil
1✔
313
}
314

315
func (d *Deployments) ProvisionTenant(ctx context.Context, tenant_id string) error {
1✔
316
        if err := d.db.ProvisionTenant(ctx, tenant_id); err != nil {
2✔
317
                return errors.Wrap(err, "failed to provision tenant")
1✔
318
        }
1✔
319

320
        return nil
1✔
321
}
322

323
// CreateImage parses artifact and uploads artifact file to the file storage - in parallel,
324
// and creates image structure in the system.
325
// Returns image ID and nil on success.
326
func (d *Deployments) CreateImage(ctx context.Context,
UNCOV
327
        multipartUploadMsg *model.MultipartUploadMsg) (string, error) {
×
UNCOV
328
        return d.handleArtifact(ctx, multipartUploadMsg, false, nil)
×
UNCOV
329
}
×
330

UNCOV
331
func (d *Deployments) saveUpdateTypes(ctx context.Context, image *model.Image) {
×
UNCOV
332
        l := log.FromContext(ctx)
×
UNCOV
333
        if image != nil && image.ArtifactMeta != nil && len(image.ArtifactMeta.Updates) > 0 {
×
UNCOV
334
                i := 0
×
UNCOV
335
                updateTypes := make([]string, len(image.ArtifactMeta.Updates))
×
UNCOV
336
                for _, t := range image.ArtifactMeta.Updates {
×
UNCOV
337
                        if t.TypeInfo.Type == nil {
×
UNCOV
338
                                continue
×
339
                        }
UNCOV
340
                        updateTypes[i] = *t.TypeInfo.Type
×
UNCOV
341
                        i++
×
342
                }
UNCOV
343
                err := d.db.SaveUpdateTypes(ctx, updateTypes[:i])
×
UNCOV
344
                if err != nil {
×
345
                        l.Errorf(
×
346
                                "error while saving the update types for the artifact: %s",
×
347
                                err.Error(),
×
348
                        )
×
349
                }
×
350
        }
351
}
352

353
// handleArtifact parses artifact and uploads artifact file to the file storage - in parallel,
354
// and creates image structure in the system.
355
// Returns image ID, artifact file ID and nil on success.
356
func (d *Deployments) handleArtifact(ctx context.Context,
357
        multipartUploadMsg *model.MultipartUploadMsg,
358
        skipVerify bool,
359
        metadata *model.DirectUploadMetadata,
360
) (string, error) {
1✔
361

1✔
362
        l := log.FromContext(ctx)
1✔
363
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
364
        if err != nil {
1✔
365
                return "", err
×
366
        }
×
367

368
        // create pipe
369
        pR, pW := io.Pipe()
1✔
370

1✔
371
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
1✔
372

1✔
373
        tee := io.TeeReader(artifactReader, pW)
1✔
374

1✔
375
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
1✔
376
        if err != nil {
1✔
UNCOV
377
                uid, _ = uuid.NewRandom()
×
UNCOV
378
        }
×
379
        artifactID := uid.String()
1✔
380

1✔
381
        ch := make(chan error)
1✔
382
        // create goroutine for artifact upload
1✔
383
        //
1✔
384
        // reading from the pipe (which is done in UploadArtifact method) is a blocking operation
1✔
385
        // and cannot be done in the same goroutine as writing to the pipe
1✔
386
        //
1✔
387
        // uploading and parsing artifact in the same process will cause in a deadlock!
1✔
388
        //nolint:errcheck
1✔
389
        go func() (err error) {
2✔
390
                defer func() { ch <- err }()
2✔
391
                if skipVerify {
2✔
392
                        err = nil
1✔
393
                        io.Copy(io.Discard, pR)
1✔
394
                        return nil
1✔
395
                }
1✔
396
                err = d.objectStorage.PutObject(
1✔
397
                        ctx, model.ImagePathFromContext(ctx, artifactID), pR,
1✔
398
                )
1✔
399
                if err != nil {
1✔
UNCOV
400
                        pR.CloseWithError(err)
×
UNCOV
401
                }
×
402
                return err
1✔
403
        }()
404

405
        // parse artifact
406
        // artifact library reads all the data from the given reader
407
        metaArtifactConstructor, err := getMetaFromArchive(&tee, skipVerify)
1✔
408
        if err != nil {
2✔
409
                _ = pW.CloseWithError(err)
1✔
410
                <-ch
1✔
411
                return artifactID, errors.Wrap(ErrModelParsingArtifactFailed, err.Error())
1✔
412
        }
1✔
UNCOV
413
        validMetadata := false
×
UNCOV
414
        if skipVerify && metadata != nil {
×
UNCOV
415
                // this means we got files and metadata separately
×
UNCOV
416
                // we can now put it in the metaArtifactConstructor
×
UNCOV
417
                // after validating that the files information match the artifact
×
UNCOV
418
                validMetadata = validUpdates(metaArtifactConstructor.Updates, metadata.Updates)
×
UNCOV
419
                if validMetadata {
×
UNCOV
420
                        metaArtifactConstructor.Updates = metadata.Updates
×
UNCOV
421
                }
×
422
        }
423
        // validate artifact metadata
UNCOV
424
        if err = metaArtifactConstructor.Validate(); err != nil {
×
425
                return artifactID, ErrModelInvalidMetadata
×
426
        }
×
427

UNCOV
428
        if !skipVerify {
×
UNCOV
429
                // read the rest of the data,
×
UNCOV
430
                // just in case the artifact library did not read all the data from the reader
×
UNCOV
431
                _, err = io.Copy(io.Discard, tee)
×
UNCOV
432
                if err != nil {
×
433
                        // CloseWithError will cause the reading end to abort upload.
×
434
                        _ = pW.CloseWithError(err)
×
435
                        <-ch
×
436
                        return artifactID, err
×
437
                }
×
438
        }
439

440
        // close the pipe
UNCOV
441
        pW.Close()
×
UNCOV
442

×
UNCOV
443
        // collect output from the goroutine
×
UNCOV
444
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
×
445
                return artifactID, uploadResponseErr
×
446
        }
×
447

UNCOV
448
        size := artifactReader.Count()
×
UNCOV
449
        if skipVerify && validMetadata {
×
UNCOV
450
                size = metadata.Size
×
UNCOV
451
        }
×
UNCOV
452
        image := model.NewImage(
×
UNCOV
453
                artifactID,
×
UNCOV
454
                multipartUploadMsg.MetaConstructor,
×
UNCOV
455
                metaArtifactConstructor,
×
UNCOV
456
                size,
×
UNCOV
457
        )
×
UNCOV
458

×
UNCOV
459
        // save image structure in the system
×
UNCOV
460
        if err = d.db.InsertImage(ctx, image); err != nil {
×
461
                // Try to remove the storage from s3.
×
462
                if errDelete := d.objectStorage.DeleteObject(
×
463
                        ctx, model.ImagePathFromContext(ctx, artifactID),
×
464
                ); errDelete != nil {
×
465
                        l.Errorf(
×
466
                                "failed to clean up artifact storage after failure: %s",
×
467
                                errDelete,
×
468
                        )
×
469
                }
×
470
                if idxErr, ok := err.(*model.ConflictError); ok {
×
471
                        return artifactID, idxErr
×
472
                }
×
473
                return artifactID, errors.Wrap(err, "Fail to store the metadata")
×
474
        }
UNCOV
475
        d.saveUpdateTypes(ctx, image)
×
UNCOV
476

×
UNCOV
477
        // update release
×
UNCOV
478
        if err := d.updateRelease(ctx, image, nil); err != nil {
×
479
                return "", err
×
480
        }
×
481

UNCOV
482
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
×
483
                return "", errors.Wrap(err, "fail to update deployments")
×
484
        }
×
485

UNCOV
486
        return artifactID, nil
×
487
}
488

UNCOV
489
func validUpdates(constructorUpdates []model.Update, metadataUpdates []model.Update) bool {
×
UNCOV
490
        valid := false
×
UNCOV
491
        if len(constructorUpdates) == len(metadataUpdates) {
×
UNCOV
492
                valid = true
×
UNCOV
493
                for _, update := range constructorUpdates {
×
UNCOV
494
                        for _, updateExternal := range metadataUpdates {
×
UNCOV
495
                                if !update.Match(updateExternal) {
×
496
                                        valid = false
×
497
                                        break
×
498
                                }
499
                        }
500
                }
501
        }
UNCOV
502
        return valid
×
503
}
504

505
// GenerateImage parses raw data and uploads it to the file storage - in parallel,
506
// creates image structure in the system, and starts the workflow to generate the
507
// artifact from them.
508
// Returns image ID and nil on success.
509
func (d *Deployments) GenerateImage(ctx context.Context,
510
        multipartGenerateImageMsg *model.MultipartGenerateImageMsg) (string, error) {
1✔
511

1✔
512
        if multipartGenerateImageMsg == nil {
2✔
513
                return "", ErrModelMultipartUploadMsgMalformed
1✔
514
        }
1✔
515

516
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
1✔
517
        if err != nil {
2✔
518
                return "", err
1✔
519
        }
1✔
520
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
2✔
521
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
522
        }
1✔
523
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
1✔
524
        if err != nil {
2✔
525
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
2✔
526
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
527
                }
1✔
528
                return "", err
1✔
529
        }
530

531
        return multipartGenerateImageMsg.ArtifactID, err
1✔
532
}
533

534
func (d *Deployments) GenerateConfigurationImage(
535
        ctx context.Context,
536
        deviceType string,
537
        deploymentID string,
538
) (io.Reader, error) {
1✔
539
        var buf bytes.Buffer
1✔
540
        dpl, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
541
        if err != nil {
2✔
542
                return nil, err
1✔
543
        } else if dpl == nil {
3✔
544
                return nil, ErrModelDeploymentNotFound
1✔
545
        }
1✔
546
        var metaData map[string]interface{}
1✔
547
        err = json.Unmarshal(dpl.Configuration, &metaData)
1✔
548
        if err != nil {
2✔
549
                return nil, errors.Wrapf(err, "malformed configuration in deployment")
1✔
550
        }
1✔
551

552
        artieWriter := awriter.NewWriter(&buf, artifact.NewCompressorNone())
1✔
553
        module := handlers.NewModuleImage(ArtifactConfigureType)
1✔
554
        err = artieWriter.WriteArtifact(&awriter.WriteArtifactArgs{
1✔
555
                Format:  "mender",
1✔
556
                Version: 3,
1✔
557
                Devices: []string{deviceType},
1✔
558
                Name:    dpl.ArtifactName,
1✔
559
                Updates: &awriter.Updates{Updates: []handlers.Composer{module}},
1✔
560
                Depends: &artifact.ArtifactDepends{
1✔
561
                        CompatibleDevices: []string{deviceType},
1✔
562
                },
1✔
563
                Provides: &artifact.ArtifactProvides{
1✔
564
                        ArtifactName: dpl.ArtifactName,
1✔
565
                },
1✔
566
                MetaData: metaData,
1✔
567
                TypeInfoV3: &artifact.TypeInfoV3{
1✔
568
                        Type: &ArtifactConfigureType,
1✔
569
                        ArtifactProvides: artifact.TypeInfoProvides{
1✔
570
                                ArtifactConfigureProvides: dpl.ArtifactName,
1✔
571
                        },
1✔
572
                        ArtifactDepends:        artifact.TypeInfoDepends{},
1✔
573
                        ClearsArtifactProvides: []string{ArtifactConfigureProvidesCleared},
1✔
574
                },
1✔
575
        })
1✔
576

1✔
577
        return &buf, err
1✔
578
}
579

580
// handleRawFile parses raw data, uploads it to the file storage,
581
// and starts the workflow to generate the artifact.
582
// Returns the object path to the file and nil on success.
583
func (d *Deployments) handleRawFile(ctx context.Context,
584
        multipartMsg *model.MultipartGenerateImageMsg) (filePath string, err error) {
1✔
585
        l := log.FromContext(ctx)
1✔
586
        uid, _ := uuid.NewRandom()
1✔
587
        artifactID := uid.String()
1✔
588
        multipartMsg.ArtifactID = artifactID
1✔
589
        filePath = model.ImagePathFromContext(ctx, artifactID+fileSuffixTmp)
1✔
590

1✔
591
        // check if artifact is unique
1✔
592
        // artifact is considered to be unique if there is no artifact with the same name
1✔
593
        // and supporting the same platform in the system
1✔
594
        isArtifactUnique, err := d.db.IsArtifactUnique(ctx,
1✔
595
                multipartMsg.Name,
1✔
596
                multipartMsg.DeviceTypesCompatible,
1✔
597
        )
1✔
598
        if err != nil {
2✔
599
                return "", errors.Wrap(err, "Fail to check if artifact is unique")
1✔
600
        }
1✔
601
        if !isArtifactUnique {
2✔
602
                return "", ErrModelArtifactNotUnique
1✔
603
        }
1✔
604

605
        ctx, err = d.contextWithStorageSettings(ctx)
1✔
606
        if err != nil {
1✔
607
                return "", err
×
608
        }
×
609
        err = d.objectStorage.PutObject(
1✔
610
                ctx, filePath, multipartMsg.FileReader,
1✔
611
        )
1✔
612
        if err != nil {
2✔
613
                return "", err
1✔
614
        }
1✔
615
        defer func() {
2✔
616
                if err != nil {
2✔
617
                        e := d.objectStorage.DeleteObject(ctx, filePath)
1✔
618
                        if e != nil {
2✔
619
                                l.Errorf("error cleaning up raw file '%s' from objectstorage: %s",
1✔
620
                                        filePath, e)
1✔
621
                        }
1✔
622
                }
623
        }()
624

625
        link, err := d.objectStorage.GetRequest(
1✔
626
                ctx,
1✔
627
                filePath,
1✔
628
                path.Base(filePath),
1✔
629
                DefaultImageGenerationLinkExpire,
1✔
630
        )
1✔
631
        if err != nil {
2✔
632
                return "", err
1✔
633
        }
1✔
634
        multipartMsg.GetArtifactURI = link.Uri
1✔
635

1✔
636
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
1✔
637
        if err != nil {
2✔
638
                return "", err
1✔
639
        }
1✔
640
        multipartMsg.DeleteArtifactURI = link.Uri
1✔
641

1✔
642
        return artifactID, nil
1✔
643
}
644

645
// GetImage allows to fetch image object with specified id
646
// Nil if not found
UNCOV
647
func (d *Deployments) GetImage(ctx context.Context, id string) (*model.Image, error) {
×
UNCOV
648

×
UNCOV
649
        image, err := d.db.FindImageByID(ctx, id)
×
UNCOV
650
        if err != nil {
×
651
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
652
        }
×
653

UNCOV
654
        if image == nil {
×
UNCOV
655
                return nil, nil
×
UNCOV
656
        }
×
657

UNCOV
658
        return image, nil
×
659
}
660

661
// DeleteImage removes metadata and image file
662
// Noop for not existing images
663
// Allowed to remove image only if image is not scheduled or in progress for an updates - then image
664
// file is needed
665
// In case of already finished updates only image file is not needed, metadata is attached directly
666
// to device deployment therefore we still have some information about image that have been used
667
// (but not the file)
UNCOV
668
func (d *Deployments) DeleteImage(ctx context.Context, imageID string) error {
×
UNCOV
669
        found, err := d.GetImage(ctx, imageID)
×
UNCOV
670

×
UNCOV
671
        if err != nil {
×
672
                return errors.Wrap(err, "Getting image metadata")
×
673
        }
×
674

UNCOV
675
        if found == nil {
×
676
                return ErrImageMetaNotFound
×
677
        }
×
678

UNCOV
679
        inUse, err := d.ImageUsedInActiveDeployment(ctx, imageID)
×
UNCOV
680
        if err != nil {
×
681
                return errors.Wrap(err, "Checking if image is used in active deployment")
×
682
        }
×
683

684
        // Image is in use, not allowed to delete
UNCOV
685
        if inUse {
×
UNCOV
686
                return ErrModelImageInActiveDeployment
×
UNCOV
687
        }
×
688

689
        // Delete image file (call to external service)
690
        // Noop for not existing file
UNCOV
691
        ctx, err = d.contextWithStorageSettings(ctx)
×
UNCOV
692
        if err != nil {
×
693
                return err
×
694
        }
×
UNCOV
695
        imagePath := model.ImagePathFromContext(ctx, imageID)
×
UNCOV
696
        if err := d.objectStorage.DeleteObject(ctx, imagePath); err != nil {
×
697
                return errors.Wrap(err, "Deleting image file")
×
698
        }
×
699

700
        // Delete metadata
UNCOV
701
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
×
702
                return errors.Wrap(err, "Deleting image metadata")
×
703
        }
×
704

705
        // update release
UNCOV
706
        if err := d.updateRelease(ctx, nil, found); err != nil {
×
707
                return err
×
708
        }
×
709

UNCOV
710
        return nil
×
711
}
712

713
// ListImages according to specified filers.
714
func (d *Deployments) ListImages(
715
        ctx context.Context,
716
        filters *model.ReleaseOrImageFilter,
UNCOV
717
) ([]*model.Image, int, error) {
×
UNCOV
718
        imageList, count, err := d.db.ListImages(ctx, filters)
×
UNCOV
719
        if err != nil {
×
720
                return nil, 0, errors.Wrap(err, "Searching for image metadata")
×
721
        }
×
722

UNCOV
723
        if imageList == nil {
×
UNCOV
724
                return make([]*model.Image, 0), 0, nil
×
UNCOV
725
        }
×
726

UNCOV
727
        return imageList, count, nil
×
728
}
729

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

×
734
        if err := constructor.Validate(); err != nil {
×
735
                return false, errors.Wrap(err, "Validating image metadata")
×
736
        }
×
737

738
        found, err := d.ImageUsedInDeployment(ctx, imageID)
×
739
        if err != nil {
×
740
                return false, errors.Wrap(err, "Searching for usage of the image among deployments")
×
741
        }
×
742

743
        if found {
×
744
                return false, ErrModelImageUsedInAnyDeployment
×
745
        }
×
746

747
        foundImage, err := d.db.FindImageByID(ctx, imageID)
×
748
        if err != nil {
×
749
                return false, errors.Wrap(err, "Searching for image with specified ID")
×
750
        }
×
751

752
        if foundImage == nil {
×
753
                return false, nil
×
754
        }
×
755

756
        foundImage.SetModified(time.Now())
×
757
        foundImage.ImageMeta = constructor
×
758

×
759
        _, err = d.db.Update(ctx, foundImage)
×
760
        if err != nil {
×
761
                return false, errors.Wrap(err, "Updating image matadata")
×
762
        }
×
763

764
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
765
                return false, err
×
766
        }
×
767

768
        return true, nil
×
769
}
770

771
// DownloadLink presigned GET link to download image file.
772
// Returns error if image have not been uploaded.
773
func (d *Deployments) DownloadLink(ctx context.Context, imageID string,
UNCOV
774
        expire time.Duration) (*model.Link, error) {
×
UNCOV
775

×
UNCOV
776
        image, err := d.GetImage(ctx, imageID)
×
UNCOV
777
        if err != nil {
×
778
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
779
        }
×
780

UNCOV
781
        if image == nil {
×
782
                return nil, nil
×
783
        }
×
784

UNCOV
785
        ctx, err = d.contextWithStorageSettings(ctx)
×
UNCOV
786
        if err != nil {
×
787
                return nil, err
×
788
        }
×
UNCOV
789
        imagePath := model.ImagePathFromContext(ctx, imageID)
×
UNCOV
790
        _, err = d.objectStorage.StatObject(ctx, imagePath)
×
UNCOV
791
        if err != nil {
×
792
                return nil, errors.Wrap(err, "Searching for image file")
×
793
        }
×
794

UNCOV
795
        link, err := d.objectStorage.GetRequest(
×
UNCOV
796
                ctx,
×
UNCOV
797
                imagePath,
×
UNCOV
798
                image.Name+model.ArtifactFileSuffix,
×
UNCOV
799
                expire,
×
UNCOV
800
        )
×
UNCOV
801
        if err != nil {
×
802
                return nil, errors.Wrap(err, "Generating download link")
×
803
        }
×
804

UNCOV
805
        return link, nil
×
806
}
807

808
func (d *Deployments) UploadLink(
809
        ctx context.Context,
810
        expire time.Duration,
811
        skipVerify bool,
812
) (*model.UploadLink, error) {
1✔
813
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
814
        if err != nil {
2✔
815
                return nil, err
1✔
816
        }
1✔
817

818
        artifactID := uuid.New().String()
1✔
819
        path := model.ImagePathFromContext(ctx, artifactID) + fileSuffixTmp
1✔
820
        if skipVerify {
1✔
UNCOV
821
                path = model.ImagePathFromContext(ctx, artifactID)
×
UNCOV
822
        }
×
823
        link, err := d.objectStorage.PutRequest(ctx, path, expire)
1✔
824
        if err != nil {
2✔
825
                return nil, errors.WithMessage(err, "app: failed to generate signed URL")
1✔
826
        }
1✔
827
        upLink := &model.UploadLink{
1✔
828
                ArtifactID: artifactID,
1✔
829
                IssuedAt:   time.Now(),
1✔
830
                Link:       *link,
1✔
831
        }
1✔
832
        err = d.db.InsertUploadIntent(ctx, upLink)
1✔
833
        if err != nil {
2✔
834
                return nil, errors.WithMessage(err, "app: error recording the upload intent")
1✔
835
        }
1✔
836

837
        return upLink, err
1✔
838
}
839

840
func (d *Deployments) processUploadedArtifact(
841
        ctx context.Context,
842
        artifactID string,
843
        artifact io.ReadCloser,
844
        skipVerify bool,
845
        metadata *model.DirectUploadMetadata,
846
) error {
1✔
847
        linkStatus := model.LinkStatusCompleted
1✔
848

1✔
849
        l := log.FromContext(ctx)
1✔
850
        defer artifact.Close()
1✔
851
        ctx, cancel := context.WithCancel(ctx)
1✔
852
        defer cancel()
1✔
853
        go func() { // Heatbeat routine
2✔
854
                ticker := time.NewTicker(inprogressIdleTime / 2)
1✔
855
                done := ctx.Done()
1✔
856
                defer ticker.Stop()
1✔
857
                for {
2✔
858
                        select {
1✔
859
                        case <-ticker.C:
×
860
                                err := d.db.UpdateUploadIntentStatus(
×
861
                                        ctx,
×
862
                                        artifactID,
×
863
                                        model.LinkStatusProcessing,
×
864
                                        model.LinkStatusProcessing,
×
865
                                )
×
866
                                if err != nil {
×
867
                                        l.Errorf("failed to update upload link timestamp: %s", err)
×
868
                                        cancel()
×
869
                                        return
×
870
                                }
×
871
                        case <-done:
1✔
872
                                return
1✔
873
                        }
874
                }
875
        }()
876
        _, err := d.handleArtifact(ctx, &model.MultipartUploadMsg{
1✔
877
                ArtifactID:     artifactID,
1✔
878
                ArtifactReader: artifact,
1✔
879
        },
1✔
880
                skipVerify,
1✔
881
                metadata,
1✔
882
        )
1✔
883
        if err != nil {
2✔
884
                l.Warnf("failed to process artifact %s: %s", artifactID, err)
1✔
885
                linkStatus = model.LinkStatusAborted
1✔
886
        }
1✔
887
        errDB := d.db.UpdateUploadIntentStatus(
1✔
888
                ctx, artifactID,
1✔
889
                model.LinkStatusProcessing, linkStatus,
1✔
890
        )
1✔
891
        if errDB != nil {
2✔
892
                l.Warnf("failed to update upload link status: %s", errDB)
1✔
893
        }
1✔
894
        return err
1✔
895
}
896

897
func (d *Deployments) CompleteUpload(
898
        ctx context.Context,
899
        intentID string,
900
        skipVerify bool,
901
        metadata *model.DirectUploadMetadata,
902
) error {
1✔
903
        l := log.FromContext(ctx)
1✔
904
        idty := identity.FromContext(ctx)
1✔
905
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
906
        if err != nil {
2✔
907
                return err
1✔
908
        }
1✔
909
        // Create an async context that doesn't cancel when server connection
910
        // closes.
911
        ctxAsync := context.Background()
1✔
912
        ctxAsync = log.WithContext(ctxAsync, l)
1✔
913
        ctxAsync = identity.WithContext(ctxAsync, idty)
1✔
914

1✔
915
        settings, _ := storage.SettingsFromContext(ctx)
1✔
916
        ctxAsync = storage.SettingsWithContext(ctxAsync, settings)
1✔
917
        var artifactReader io.ReadCloser
1✔
918
        if skipVerify {
2✔
919
                artifactReader, err = d.objectStorage.GetObject(
1✔
920
                        ctxAsync,
1✔
921
                        model.ImagePathFromContext(ctx, intentID),
1✔
922
                )
1✔
923
        } else {
2✔
924
                artifactReader, err = d.objectStorage.GetObject(
1✔
925
                        ctxAsync,
1✔
926
                        model.ImagePathFromContext(ctx, intentID)+fileSuffixTmp,
1✔
927
                )
1✔
928
        }
1✔
929
        if err != nil {
2✔
930
                if errors.Is(err, storage.ErrObjectNotFound) {
2✔
931
                        return ErrUploadNotFound
1✔
932
                }
1✔
933
                return err
1✔
934
        }
935

936
        err = d.db.UpdateUploadIntentStatus(
1✔
937
                ctx,
1✔
938
                intentID,
1✔
939
                model.LinkStatusPending,
1✔
940
                model.LinkStatusProcessing,
1✔
941
        )
1✔
942
        if err != nil {
2✔
943
                errClose := artifactReader.Close()
1✔
944
                if errClose != nil {
2✔
945
                        l.Warnf("failed to close artifact reader: %s", errClose)
1✔
946
                }
1✔
947
                if errors.Is(err, store.ErrNotFound) {
2✔
948
                        return ErrUploadNotFound
1✔
949
                }
1✔
950
                return err
1✔
951
        }
952
        go d.processUploadedArtifact( // nolint:errcheck
1✔
953
                ctxAsync, intentID, artifactReader, skipVerify, metadata,
1✔
954
        )
1✔
955
        return nil
1✔
956
}
957

UNCOV
958
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
×
UNCOV
959
        return &model.ArtifactInfo{
×
UNCOV
960
                Format:  info.Format,
×
UNCOV
961
                Version: uint(info.Version),
×
UNCOV
962
        }
×
UNCOV
963
}
×
964

UNCOV
965
func getUpdateFiles(uFiles []*handlers.DataFile) ([]model.UpdateFile, error) {
×
UNCOV
966
        var files []model.UpdateFile
×
UNCOV
967
        for _, u := range uFiles {
×
UNCOV
968
                files = append(files, model.UpdateFile{
×
UNCOV
969
                        Name:     u.Name,
×
UNCOV
970
                        Size:     u.Size,
×
UNCOV
971
                        Date:     &u.Date,
×
UNCOV
972
                        Checksum: string(u.Checksum),
×
UNCOV
973
                })
×
UNCOV
974
        }
×
UNCOV
975
        return files, nil
×
976
}
977

978
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
1✔
979
        metaArtifact := model.NewArtifactMeta()
1✔
980

1✔
981
        aReader := areader.NewReader(*r)
1✔
982

1✔
983
        // There is no signature verification here.
1✔
984
        // It is just simple check if artifact is signed or not.
1✔
985
        aReader.VerifySignatureCallback = func(message, sig []byte) error {
1✔
986
                metaArtifact.Signed = true
×
987
                return nil
×
988
        }
×
989

990
        var err error
1✔
991
        if skipVerify {
2✔
992
                err = aReader.ReadArtifactHeaders()
1✔
993
                if err != nil {
2✔
994
                        return nil, errors.Wrap(err, "reading artifact error")
1✔
995
                }
1✔
996
        } else {
1✔
997
                err = aReader.ReadArtifact()
1✔
998
                if err != nil {
2✔
999
                        return nil, errors.Wrap(err, "reading artifact error")
1✔
1000
                }
1✔
1001
        }
1002

UNCOV
1003
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
×
UNCOV
1004
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
×
UNCOV
1005

×
UNCOV
1006
        metaArtifact.Name = aReader.GetArtifactName()
×
UNCOV
1007
        if metaArtifact.Info.Version == 3 {
×
UNCOV
1008
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
×
UNCOV
1009
                if err != nil {
×
1010
                        return nil, errors.Wrap(err,
×
1011
                                "error parsing version 3 artifact")
×
1012
                }
×
1013

UNCOV
1014
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
×
UNCOV
1015
                if err != nil {
×
1016
                        return nil, errors.Wrap(err,
×
1017
                                "error parsing version 3 artifact")
×
1018
                }
×
1019

UNCOV
1020
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
×
1021
        }
1022

UNCOV
1023
        for _, p := range aReader.GetHandlers() {
×
UNCOV
1024
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
×
UNCOV
1025
                if err != nil {
×
1026
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
1027
                }
×
1028

UNCOV
1029
                uMetadata, err := p.GetUpdateMetaData()
×
UNCOV
1030
                if err != nil {
×
1031
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
1032
                }
×
1033

UNCOV
1034
                metaArtifact.Updates = append(
×
UNCOV
1035
                        metaArtifact.Updates,
×
UNCOV
1036
                        model.Update{
×
UNCOV
1037
                                TypeInfo: model.ArtifactUpdateTypeInfo{
×
UNCOV
1038
                                        Type: p.GetUpdateType(),
×
UNCOV
1039
                                },
×
UNCOV
1040
                                Files:    uFiles,
×
UNCOV
1041
                                MetaData: uMetadata,
×
UNCOV
1042
                        })
×
1043
        }
1044

UNCOV
1045
        return metaArtifact, nil
×
1046
}
1047

1048
func getArtifactIDs(artifacts []*model.Image) []string {
1✔
1049
        artifactIDs := make([]string, 0, len(artifacts))
1✔
1050
        for _, artifact := range artifacts {
2✔
1051
                artifactIDs = append(artifactIDs, artifact.Id)
1✔
1052
        }
1✔
1053
        return artifactIDs
1✔
1054
}
1055

1056
// deployments
1057
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
1✔
1058
        ids := make([]string, len(devices))
1✔
1059
        for i, d := range devices {
2✔
1060
                ids[i] = d.ID
1✔
1061
        }
1✔
1062

1063
        return ids
1✔
1064
}
1065

1066
// updateDeploymentConstructor fills devices list with device ids
1067
func (d *Deployments) updateDeploymentConstructor(ctx context.Context,
1068
        constructor *model.DeploymentConstructor) (*model.DeploymentConstructor, error) {
1✔
1069
        l := log.FromContext(ctx)
1✔
1070

1✔
1071
        id := identity.FromContext(ctx)
1✔
1072
        if id == nil {
1✔
1073
                l.Error("identity not present in the context")
×
1074
                return nil, ErrModelInternal
×
1075
        }
×
1076
        searchParams := model.SearchParams{
1✔
1077
                Page:    1,
1✔
1078
                PerPage: PerPageInventoryDevices,
1✔
1079
                Filters: []model.FilterPredicate{
1✔
1080
                        {
1✔
1081
                                Scope:     InventoryIdentityScope,
1✔
1082
                                Attribute: InventoryStatusAttributeName,
1✔
1083
                                Type:      "$eq",
1✔
1084
                                Value:     InventoryStatusAccepted,
1✔
1085
                        },
1✔
1086
                },
1✔
1087
        }
1✔
1088
        if len(constructor.Group) > 0 {
2✔
1089
                searchParams.Filters = append(
1✔
1090
                        searchParams.Filters,
1✔
1091
                        model.FilterPredicate{
1✔
1092
                                Scope:     InventoryGroupScope,
1✔
1093
                                Attribute: InventoryGroupAttributeName,
1✔
1094
                                Type:      "$eq",
1✔
1095
                                Value:     constructor.Group,
1✔
1096
                        })
1✔
1097
        }
1✔
1098

1099
        for {
2✔
1100
                devices, count, err := d.search(ctx, id.Tenant, searchParams)
1✔
1101
                if err != nil {
2✔
1102
                        l.Errorf("error searching for devices")
1✔
1103
                        return nil, ErrModelInternal
1✔
1104
                }
1✔
1105
                if count < 1 {
2✔
1106
                        l.Errorf("no devices found")
1✔
1107
                        return nil, ErrNoDevices
1✔
1108
                }
1✔
1109
                if len(devices) < 1 {
1✔
1110
                        break
×
1111
                }
1112
                constructor.Devices = append(constructor.Devices, inventoryDevicesToDevicesIds(devices)...)
1✔
1113
                if len(constructor.Devices) == count {
2✔
1114
                        break
1✔
1115
                }
1116
                searchParams.Page++
1✔
1117
        }
1118

1119
        return constructor, nil
1✔
1120
}
1121

1122
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1123
func (d *Deployments) CreateDeviceConfigurationDeployment(
1124
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1125
        deviceID, deploymentID string) (string, error) {
1✔
1126

1✔
1127
        if constructor == nil {
2✔
1128
                return "", ErrModelMissingInput
1✔
1129
        }
1✔
1130

1131
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
1✔
1132
                constructor,
1✔
1133
                deploymentID,
1✔
1134
        )
1✔
1135
        if err != nil {
1✔
1136
                return "", errors.Wrap(err, "failed to create deployment")
×
1137
        }
×
1138

1139
        deployment.DeviceList = []string{deviceID}
1✔
1140
        deployment.MaxDevices = 1
1✔
1141
        deployment.Configuration = []byte(constructor.Configuration)
1✔
1142
        deployment.Type = model.DeploymentTypeConfiguration
1✔
1143

1✔
1144
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
1✔
1145
        if err != nil {
2✔
1146
                return "", err
1✔
1147
        }
1✔
1148
        deployment.Groups = groups
1✔
1149

1✔
1150
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
2✔
1151
                if err == mongo.ErrConflictingDeployment {
1✔
UNCOV
1152
                        return "", ErrDuplicateDeployment
×
UNCOV
1153
                }
×
1154
                if strings.Contains(err.Error(), "id: must be a valid UUID") {
1✔
UNCOV
1155
                        return "", ErrInvalidDeploymentID
×
UNCOV
1156
                }
×
1157
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1158
        }
1159

1160
        return deployment.Id, nil
1✔
1161
}
1162

1163
// CreateDeployment precomputes new deployment and schedules it for devices.
1164
func (d *Deployments) CreateDeployment(ctx context.Context,
1165
        constructor *model.DeploymentConstructor) (string, error) {
1✔
1166

1✔
1167
        var err error
1✔
1168

1✔
1169
        if constructor == nil {
2✔
1170
                return "", ErrModelMissingInput
1✔
1171
        }
1✔
1172

1173
        if err := constructor.Validate(); err != nil {
1✔
1174
                return "", errors.Wrap(err, "Validating deployment")
×
1175
        }
×
1176

1177
        if len(constructor.Group) > 0 || constructor.AllDevices {
2✔
1178
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
1✔
1179
                if err != nil {
2✔
1180
                        return "", err
1✔
1181
                }
1✔
1182
        }
1183

1184
        deployment, err := model.NewDeploymentFromConstructor(constructor)
1✔
1185
        if err != nil {
1✔
1186
                return "", errors.Wrap(err, "failed to create deployment")
×
1187
        }
×
1188

1189
        // Assign artifacts to the deployment.
1190
        // When new artifact(s) with the artifact name same as the one in the deployment
1191
        // will be uploaded to the backend, it will also become part of this deployment.
1192
        artifacts, err := d.db.ImagesByName(ctx, deployment.ArtifactName)
1✔
1193
        if err != nil {
1✔
1194
                return "", errors.Wrap(err, "Finding artifact with given name")
×
1195
        }
×
1196

1197
        if len(artifacts) == 0 {
1✔
UNCOV
1198
                return "", ErrNoArtifact
×
UNCOV
1199
        }
×
1200

1201
        deployment.Artifacts = getArtifactIDs(artifacts)
1✔
1202
        deployment.DeviceList = constructor.Devices
1✔
1203
        deployment.MaxDevices = len(constructor.Devices)
1✔
1204
        deployment.Type = model.DeploymentTypeSoftware
1✔
1205
        deployment.Filter = getDeploymentFilter(constructor)
1✔
1206
        if len(constructor.Group) > 0 {
2✔
1207
                deployment.Groups = []string{constructor.Group}
1✔
1208
        }
1✔
1209

1210
        // single device deployment case
1211
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
2✔
1212
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
1✔
1213
                if err != nil {
1✔
1214
                        return "", err
×
1215
                }
×
1216
                deployment.Groups = groups
1✔
1217
        }
1218

1219
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
2✔
1220
                if err == mongo.ErrConflictingDeployment {
2✔
1221
                        return "", ErrConflictingDeployment
1✔
1222
                }
1✔
1223
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1224
        }
1225

1226
        return deployment.Id, nil
1✔
1227
}
1228

1229
func (d *Deployments) getDeploymentGroups(
1230
        ctx context.Context,
1231
        devices []string,
1232
) ([]string, error) {
1✔
1233
        id := identity.FromContext(ctx)
1✔
1234

1✔
1235
        //only for single device deployment case
1✔
1236
        if len(devices) != 1 {
1✔
1237
                return nil, nil
×
1238
        }
×
1239

1240
        if id == nil {
1✔
UNCOV
1241
                id = &identity.Identity{}
×
UNCOV
1242
        }
×
1243

1244
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
1✔
1245
        if err != nil && err != inventory.ErrDevNotFound {
2✔
1246
                return nil, err
1✔
1247
        }
1✔
1248
        return groups, nil
1✔
1249
}
1250

1251
func getDeploymentFilter(
1252
        constructor *model.DeploymentConstructor,
1253
) *model.Filter {
1✔
1254

1✔
1255
        var filter *model.Filter
1✔
1256

1✔
1257
        if len(constructor.Group) > 0 {
2✔
1258
                filter = &model.Filter{
1✔
1259
                        Terms: []model.FilterPredicate{
1✔
1260
                                {
1✔
1261
                                        Scope:     InventoryGroupScope,
1✔
1262
                                        Attribute: InventoryGroupAttributeName,
1✔
1263
                                        Type:      "$eq",
1✔
1264
                                        Value:     constructor.Group,
1✔
1265
                                },
1✔
1266
                        },
1✔
1267
                }
1✔
1268
        } else if constructor.AllDevices {
3✔
1269
                filter = &model.Filter{
1✔
1270
                        Terms: []model.FilterPredicate{
1✔
1271
                                {
1✔
1272
                                        Scope:     InventoryIdentityScope,
1✔
1273
                                        Attribute: InventoryStatusAttributeName,
1✔
1274
                                        Type:      "$eq",
1✔
1275
                                        Value:     InventoryStatusAccepted,
1✔
1276
                                },
1✔
1277
                        },
1✔
1278
                }
1✔
1279
        } else if len(constructor.Devices) > 0 {
3✔
1280
                filter = &model.Filter{
1✔
1281
                        Terms: []model.FilterPredicate{
1✔
1282
                                {
1✔
1283
                                        Scope:     InventoryIdentityScope,
1✔
1284
                                        Attribute: InventoryIdAttributeName,
1✔
1285
                                        Type:      "$in",
1✔
1286
                                        Value:     constructor.Devices,
1✔
1287
                                },
1✔
1288
                        },
1✔
1289
                }
1✔
1290
        }
1✔
1291

1292
        return filter
1✔
1293
}
1294

1295
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1296
func (d *Deployments) IsDeploymentFinished(
1297
        ctx context.Context,
1298
        deploymentID string,
UNCOV
1299
) (bool, error) {
×
UNCOV
1300
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
×
UNCOV
1301
        if err != nil {
×
1302
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1303
        }
×
UNCOV
1304
        if deployment == nil {
×
UNCOV
1305
                return true, nil
×
UNCOV
1306
        }
×
1307

UNCOV
1308
        return false, nil
×
1309
}
1310

1311
// GetDeployment fetches deployment by ID
1312
func (d *Deployments) GetDeployment(ctx context.Context,
UNCOV
1313
        deploymentID string) (*model.Deployment, error) {
×
UNCOV
1314

×
UNCOV
1315
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
×
UNCOV
1316
        if err != nil {
×
1317
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1318
        }
×
1319

UNCOV
1320
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1321
                return nil, err
×
1322
        }
×
1323

UNCOV
1324
        return deployment, nil
×
1325
}
1326

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

1✔
1332
        var found bool
1✔
1333

1✔
1334
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
1✔
1335
        if err != nil {
2✔
1336
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1337
        }
1✔
1338

1339
        return found, nil
1✔
1340
}
1341

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

×
1346
        var found bool
×
1347

×
1348
        found, err := d.db.ExistByArtifactId(ctx, imageID)
×
1349
        if err != nil {
×
1350
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1351
        }
×
1352

1353
        return found, nil
×
1354
}
1355

1356
// Retrieves the model.Deployment and model.DeviceDeployment structures
1357
// for the device. Upon error, nil is returned for both deployment structures.
1358
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1359
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
1360

1✔
1361
        // Retrieve device deployment
1✔
1362
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
1✔
1363

1✔
1364
        if err != nil {
1✔
1365
                return nil, nil, errors.Wrap(err,
×
1366
                        "Searching for oldest active deployment for the device")
×
1367
        } else if deviceDeployment == nil {
1✔
UNCOV
1368
                return d.getNewDeploymentForDevice(ctx, deviceID)
×
UNCOV
1369
        }
×
1370

1371
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
1✔
1372
        if err != nil {
1✔
1373
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1374
        }
×
1375
        if deployment == nil {
1✔
1376
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1377
        }
×
1378

1379
        return deployment, deviceDeployment, nil
1✔
1380
}
1381

1382
// getNewDeploymentForDevice returns deployment object and creates and returns
1383
// new device deployment for the device;
1384
//
1385
// we are interested only in the deployments that are newer than the latest
1386
// deployment applied by the device;
1387
// this way we guarantee that the device will not receive deployment
1388
// that is older than the one installed on the device;
1389
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
UNCOV
1390
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
×
UNCOV
1391

×
UNCOV
1392
        var lastDeployment *time.Time
×
UNCOV
1393
        //get latest device deployment for the device;
×
UNCOV
1394
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
×
UNCOV
1395
        if err != nil {
×
1396
                return nil, nil, errors.Wrap(err,
×
1397
                        "Searching for latest active deployment for the device")
×
UNCOV
1398
        } else if deviceDeployment == nil {
×
UNCOV
1399
                lastDeployment = &time.Time{}
×
UNCOV
1400
        } else {
×
UNCOV
1401
                lastDeployment = deviceDeployment.Created
×
UNCOV
1402
        }
×
1403

1404
        //get deployments newer then last device deployment
1405
        //iterate over deployments and check if the device is part of the deployment or not
UNCOV
1406
        var deploy *model.Deployment
×
NEW
1407
        for lastDeployment != nil {
×
NEW
1408
                deploy, err = d.db.FindNewerActiveDeployment(ctx, lastDeployment, deviceID)
×
1409
                if err != nil {
×
NEW
1410
                        return nil, nil, errors.Wrap(err, "Failed to search for newer active deployments")
×
NEW
1411
                }
×
NEW
1412
                if deploy != nil {
×
NEW
1413
                        if deploy.MaxDevices > 0 &&
×
NEW
1414
                                deploy.DeviceCount != nil &&
×
NEW
1415
                                *deploy.DeviceCount >= deploy.MaxDevices {
×
NEW
1416
                                lastDeployment = deploy.Created
×
NEW
1417
                                continue
×
1418
                        }
NEW
1419
                        deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
×
NEW
1420
                                deviceID, deploy, model.DeviceDeploymentStatusPending)
×
NEW
1421
                        if err != nil {
×
NEW
1422
                                return nil, nil, err
×
NEW
1423
                        }
×
NEW
1424
                        return deploy, deviceDeployment, nil
×
NEW
1425
                } else {
×
NEW
1426
                        lastDeployment = nil
×
UNCOV
1427
                }
×
1428
        }
UNCOV
1429
        return nil, nil, nil
×
1430
}
1431

1432
func (d *Deployments) createDeviceDeploymentWithStatus(
1433
        ctx context.Context, deviceID string,
1434
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1435
) (*model.DeviceDeployment, error) {
1✔
1436
        prevStatus := model.DeviceDeploymentStatusNull
1✔
1437
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
1✔
1438
        if err != nil && err != mongo.ErrStorageNotFound {
1✔
1439
                return nil, err
×
1440
        } else if deviceDeployment != nil {
1✔
1441
                prevStatus = deviceDeployment.Status
×
1442
        }
×
1443

1444
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
1✔
1445
        deviceDeployment.Status = status
1✔
1446
        deviceDeployment.Active = status.Active()
1✔
1447
        deviceDeployment.Created = deployment.Created
1✔
1448

1✔
1449
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1450
                return nil, err
×
1451
        }
×
1452

1453
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
1✔
1454
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
1✔
1455
                return nil, err
×
1456
        }
×
1457

1458
        if prevStatus != status {
2✔
1459
                beforeStatus := deployment.GetStatus()
1✔
1460
                // after inserting new device deployment update deployment stats
1✔
1461
                // in the database, and update deployment status
1✔
1462
                deployment.Stats, err = d.db.UpdateStatsInc(
1✔
1463
                        ctx, deployment.Id,
1✔
1464
                        prevStatus, status,
1✔
1465
                )
1✔
1466
                if err != nil {
1✔
1467
                        return nil, err
×
1468
                }
×
1469
                newStatus := deployment.GetStatus()
1✔
1470
                if beforeStatus != newStatus {
1✔
1471
                        err = d.db.SetDeploymentStatus(
×
1472
                                ctx, deployment.Id,
×
1473
                                newStatus, time.Now(),
×
1474
                        )
×
1475
                        if err != nil {
×
1476
                                return nil, errors.Wrap(err,
×
1477
                                        "failed to update deployment status")
×
1478
                        }
×
1479
                }
1480
        }
1481

1482
        if !status.Active() {
2✔
1483
                err := d.reindexDevice(ctx, deviceID)
1✔
1484
                if err != nil {
1✔
1485
                        l := log.FromContext(ctx)
×
1486
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1487
                }
×
1488
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
1489
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1490
                        l := log.FromContext(ctx)
×
1491
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1492
                }
×
1493
        }
1494

1495
        return deviceDeployment, nil
1✔
1496
}
1497

1498
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1499
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1500
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
1✔
1501

1✔
1502
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
1✔
1503
        if err != nil {
1✔
1504
                return nil, ErrModelInternal
×
1505
        } else if deployment == nil {
1✔
UNCOV
1506
                return nil, nil
×
UNCOV
1507
        }
×
1508

1509
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
1✔
1510
        if err != nil {
1✔
UNCOV
1511
                return nil, err
×
UNCOV
1512
        }
×
1513
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
1✔
1514
}
1515

1516
func (d *Deployments) getDeploymentInstructions(
1517
        ctx context.Context,
1518
        deployment *model.Deployment,
1519
        deviceDeployment *model.DeviceDeployment,
1520
        request *model.DeploymentNextRequest,
1521
) (*model.DeploymentInstructions, error) {
1✔
1522

1✔
1523
        var newArtifactAssigned bool
1✔
1524

1✔
1525
        l := log.FromContext(ctx)
1✔
1526

1✔
1527
        if deployment.Type == model.DeploymentTypeConfiguration {
1✔
UNCOV
1528
                // There's nothing more we need to do, the link must be filled
×
UNCOV
1529
                // in by the API layer.
×
UNCOV
1530
                return &model.DeploymentInstructions{
×
UNCOV
1531
                        ID: deployment.Id,
×
UNCOV
1532
                        Artifact: model.ArtifactDeploymentInstructions{
×
UNCOV
1533
                                // configuration artifacts are created on demand, so they do not have IDs
×
UNCOV
1534
                                // use deployment ID togheter with device ID as artifact ID
×
UNCOV
1535
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
×
UNCOV
1536
                                ArtifactName:          deployment.ArtifactName,
×
UNCOV
1537
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
×
UNCOV
1538
                        },
×
UNCOV
1539
                        Type: model.DeploymentTypeConfiguration,
×
UNCOV
1540
                }, nil
×
UNCOV
1541
        }
×
1542

1543
        // assing artifact to the device deployment
1544
        // only if it was not assgined previously
1545
        if deviceDeployment.Image == nil {
2✔
1546
                if err := d.assignArtifact(
1✔
1547
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
1✔
1548
                        return nil, err
×
1549
                }
×
1550
                newArtifactAssigned = true
1✔
1551
        }
1552

1553
        if deviceDeployment.Image == nil {
1✔
UNCOV
1554
                // No artifact - return empty response
×
UNCOV
1555
                return nil, nil
×
UNCOV
1556
        }
×
1557

1558
        // if the deployment is not forcing the installation, and
1559
        // if artifact was recognized as already installed, and this is
1560
        // a new device deployment - indicated by device deployment status "pending",
1561
        // handle already installed artifact case
1562
        if !deployment.ForceInstallation &&
1✔
1563
                d.isAlreadyInstalled(request, deviceDeployment) &&
1✔
1564
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
2✔
1565
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
1✔
1566
        }
1✔
1567

1568
        // if new artifact has been assigned to device deployment
1569
        // add artifact size to deployment total size,
1570
        // before returning deployment instruction to the device
UNCOV
1571
        if newArtifactAssigned {
×
UNCOV
1572
                if err := d.db.IncrementDeploymentTotalSize(
×
UNCOV
1573
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
×
1574
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1575
                }
×
1576
        }
1577

UNCOV
1578
        ctx, err := d.contextWithStorageSettings(ctx)
×
UNCOV
1579
        if err != nil {
×
1580
                return nil, err
×
1581
        }
×
1582

UNCOV
1583
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
×
UNCOV
1584
        link, err := d.objectStorage.GetRequest(
×
UNCOV
1585
                ctx,
×
UNCOV
1586
                imagePath,
×
UNCOV
1587
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
×
UNCOV
1588
                DefaultUpdateDownloadLinkExpire,
×
UNCOV
1589
        )
×
UNCOV
1590
        if err != nil {
×
1591
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1592
        }
×
1593

UNCOV
1594
        instructions := &model.DeploymentInstructions{
×
UNCOV
1595
                ID: deviceDeployment.DeploymentId,
×
UNCOV
1596
                Artifact: model.ArtifactDeploymentInstructions{
×
UNCOV
1597
                        ID: deviceDeployment.Image.Id,
×
UNCOV
1598
                        ArtifactName: deviceDeployment.Image.
×
UNCOV
1599
                                ArtifactMeta.Name,
×
UNCOV
1600
                        Source: *link,
×
UNCOV
1601
                        DeviceTypesCompatible: deviceDeployment.Image.
×
UNCOV
1602
                                ArtifactMeta.DeviceTypesCompatible,
×
UNCOV
1603
                },
×
UNCOV
1604
        }
×
UNCOV
1605

×
UNCOV
1606
        return instructions, nil
×
1607
}
1608

1609
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1610
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
1✔
1611
        if deviceDeployment.Request != nil {
1✔
UNCOV
1612
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
×
UNCOV
1613
                        // the device reported different device type and/or artifact name during the
×
UNCOV
1614
                        // update process, this can happen if the mender-store DB in the client is not
×
UNCOV
1615
                        // persistent so a new deployment start without a previous one is still ongoing;
×
UNCOV
1616
                        // mark deployment for this device as failed to force client to rollback
×
UNCOV
1617
                        l := log.FromContext(ctx)
×
UNCOV
1618
                        l.Errorf(
×
UNCOV
1619
                                "Device with id %s reported new data: %s during update process;"+
×
UNCOV
1620
                                        "old data: %s",
×
UNCOV
1621
                                deviceID, request, deviceDeployment.Request)
×
UNCOV
1622

×
UNCOV
1623
                        if err := d.updateDeviceDeploymentStatus(ctx, deviceDeployment,
×
UNCOV
1624
                                model.DeviceDeploymentState{
×
UNCOV
1625
                                        Status: model.DeviceDeploymentStatusFailure,
×
UNCOV
1626
                                }); err != nil {
×
1627
                                return errors.Wrap(err, "Failed to update deployment status")
×
1628
                        }
×
UNCOV
1629
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
×
1630
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1631
                        }
×
UNCOV
1632
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
×
UNCOV
1633
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
×
1634
                                l := log.FromContext(ctx)
×
1635
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1636
                        }
×
UNCOV
1637
                        return ErrConflictingRequestData
×
1638
                }
1639
        } else {
1✔
1640
                // save the request
1✔
1641
                if err := d.db.SaveDeviceDeploymentRequest(
1✔
1642
                        ctx, deviceDeployment.Id, request); err != nil {
1✔
1643
                        return err
×
1644
                }
×
1645
        }
1646
        return nil
1✔
1647
}
1648

1649
// updateDeviceDeploymentStatus will update the deployment status for device of
1650
// ID `deviceID`. Returns nil if update was successful.
1651
func (d *Deployments) UpdateDeviceDeploymentStatus(
1652
        ctx context.Context,
1653
        deviceID, deploymentID string,
1654
        ddState model.DeviceDeploymentState,
1655
) error {
1✔
1656
        deviceDeployment, err := d.db.GetDeviceDeployment(
1✔
1657
                ctx, deviceID, deploymentID, false,
1✔
1658
        )
1✔
1659
        if err == mongo.ErrStorageNotFound {
2✔
1660
                return ErrStorageNotFound
1✔
1661
        } else if err != nil {
2✔
1662
                return err
×
1663
        }
×
1664

1665
        return d.updateDeviceDeploymentStatus(ctx, deviceDeployment, ddState)
1✔
1666
}
1667

1668
func (d *Deployments) updateDeviceDeploymentStatus(
1669
        ctx context.Context,
1670
        dd *model.DeviceDeployment,
1671
        ddState model.DeviceDeploymentState,
1672
) error {
1✔
1673

1✔
1674
        l := log.FromContext(ctx)
1✔
1675

1✔
1676
        l.Infof("New status: %s for device %s deployment: %v",
1✔
1677
                ddState.Status, dd.DeviceId, dd.DeploymentId,
1✔
1678
        )
1✔
1679

1✔
1680
        var finishTime *time.Time = nil
1✔
1681
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
2✔
1682
                now := time.Now()
1✔
1683
                finishTime = &now
1✔
1684
        }
1✔
1685

1686
        currentStatus := dd.Status
1✔
1687

1✔
1688
        if currentStatus == model.DeviceDeploymentStatusAborted {
1✔
1689
                return ErrDeploymentAborted
×
1690
        }
×
1691

1692
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
1✔
1693
                return ErrDeviceDecommissioned
×
1694
        }
×
1695

1696
        // nothing to do
1697
        if ddState.Status == currentStatus {
1✔
1698
                return nil
×
1699
        }
×
1700

1701
        // update finish time
1702
        ddState.FinishTime = finishTime
1✔
1703

1✔
1704
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
1✔
1705
                dd.DeviceId, dd.DeploymentId, ddState, dd.Status)
1✔
1706
        if err != nil {
1✔
1707
                return err
×
1708
        }
×
1709

1710
        if old != ddState.Status {
2✔
1711
                // fetch deployment stats and update deployment status
1✔
1712
                deployment, err := d.db.FindDeploymentByID(ctx, dd.DeploymentId)
1✔
1713
                if err != nil {
1✔
1714
                        return errors.Wrap(err, "failed when searching for deployment")
×
1715
                }
×
1716
                beforeStatus := deployment.GetStatus()
1✔
1717

1✔
1718
                deployment.Stats, err = d.db.UpdateStatsInc(ctx, dd.DeploymentId, old, ddState.Status)
1✔
1719
                if err != nil {
1✔
1720
                        return err
×
1721
                }
×
1722
                newStatus := deployment.GetStatus()
1✔
1723
                if beforeStatus != newStatus {
2✔
1724
                        err = d.db.SetDeploymentStatus(ctx, dd.DeploymentId, newStatus, time.Now())
1✔
1725
                        if err != nil {
1✔
1726
                                return errors.Wrap(err, "failed to update deployment status")
×
1727
                        }
×
1728
                }
1729
        }
1730

1731
        if !ddState.Status.Active() {
2✔
1732
                l := log.FromContext(ctx)
1✔
1733
                ldd := model.DeviceDeployment{
1✔
1734
                        DeviceId:     dd.DeviceId,
1✔
1735
                        DeploymentId: dd.DeploymentId,
1✔
1736
                        Id:           dd.Id,
1✔
1737
                        Status:       ddState.Status,
1✔
1738
                }
1✔
1739
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
1✔
1740
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1741
                }
×
1742
                if err := d.reindexDevice(ctx, dd.DeviceId); err != nil {
1✔
1743
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1744
                }
×
1745
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
1✔
1746
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1747
                }
×
1748
        }
1749

1750
        return nil
1✔
1751
}
1752

1753
func (d *Deployments) GetDeploymentStats(ctx context.Context,
UNCOV
1754
        deploymentID string) (model.Stats, error) {
×
UNCOV
1755

×
UNCOV
1756
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
×
UNCOV
1757

×
UNCOV
1758
        if err != nil {
×
1759
                return nil, errors.Wrap(err, "checking deployment id")
×
1760
        }
×
1761

UNCOV
1762
        if deployment == nil {
×
1763
                return nil, nil
×
1764
        }
×
1765

UNCOV
1766
        return deployment.Stats, nil
×
1767
}
1768
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1769
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1770

×
1771
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1772

×
1773
        if err != nil {
×
1774
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1775
        }
×
1776

1777
        if deploymentStats == nil {
×
1778
                return nil, ErrModelDeploymentNotFound
×
1779
        }
×
1780

1781
        return deploymentStats, nil
×
1782
}
1783

1784
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1785
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
UNCOV
1786
        deploymentID string) ([]model.DeviceDeployment, error) {
×
UNCOV
1787

×
UNCOV
1788
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
×
UNCOV
1789
        if err != nil {
×
1790
                return nil, ErrModelInternal
×
1791
        }
×
1792

UNCOV
1793
        if deployment == nil {
×
1794
                return nil, ErrModelDeploymentNotFound
×
1795
        }
×
1796

UNCOV
1797
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
×
UNCOV
1798
        if err != nil {
×
1799
                return nil, ErrModelInternal
×
1800
        }
×
1801

UNCOV
1802
        return statuses, nil
×
1803
}
1804

1805
func (d *Deployments) GetDevicesListForDeployment(ctx context.Context,
UNCOV
1806
        query store.ListQuery) ([]model.DeviceDeployment, int, error) {
×
UNCOV
1807

×
UNCOV
1808
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
×
UNCOV
1809
        if err != nil {
×
1810
                return nil, -1, ErrModelInternal
×
1811
        }
×
1812

UNCOV
1813
        if deployment == nil {
×
1814
                return nil, -1, ErrModelDeploymentNotFound
×
1815
        }
×
1816

UNCOV
1817
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
×
UNCOV
1818
        if err != nil {
×
1819
                return nil, -1, ErrModelInternal
×
1820
        }
×
1821

UNCOV
1822
        return statuses, totalCount, nil
×
1823
}
1824

1825
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1826
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
1✔
1827
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
1✔
1828
        if err != nil {
2✔
1829
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1830
        }
1✔
1831

1832
        deploymentIDs := make([]string, len(deviceDeployments))
1✔
1833
        for i, deviceDeployment := range deviceDeployments {
2✔
1834
                deploymentIDs[i] = deviceDeployment.DeploymentId
1✔
1835
        }
1✔
1836
        var deployments []*model.Deployment
1✔
1837
        if len(deviceDeployments) > 0 {
2✔
1838
                deployments, _, err = d.db.FindDeployments(ctx, model.Query{
1✔
1839
                        IDs:          deploymentIDs,
1✔
1840
                        Limit:        len(deviceDeployments),
1✔
1841
                        DisableCount: true,
1✔
1842
                })
1✔
1843
                if err != nil {
2✔
1844
                        return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1845
                }
1✔
1846
        }
1847

1848
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
1✔
1849
        for _, deployment := range deployments {
2✔
1850
                deploymentsMap[deployment.Id] = deployment
1✔
1851
        }
1✔
1852

1853
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
1✔
1854
        for i, deviceDeployment := range deviceDeployments {
2✔
1855
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
2✔
1856
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1857
                                Id:         deviceDeployment.Id,
1✔
1858
                                Deployment: deployment,
1✔
1859
                                Device:     &deviceDeployments[i],
1✔
1860
                        })
1✔
1861
                } else {
2✔
1862
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1863
                                Id:     deviceDeployment.Id,
1✔
1864
                                Device: &deviceDeployments[i],
1✔
1865
                        })
1✔
1866
                }
1✔
1867
        }
1868

1869
        return res, totalCount, nil
1✔
1870
}
1871

1872
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1873
        ctx context.Context,
1874
        deployment *model.Deployment,
1875
) error {
1✔
1876
        if deployment.DeviceCount == nil {
1✔
1877
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1878
                if err != nil {
×
1879
                        return errors.Wrap(err, "counting device deployments")
×
1880
                }
×
1881
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1882
                if err != nil {
×
1883
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1884
                }
×
1885
                deployment.DeviceCount = &deviceCount
×
1886
        }
1887

1888
        return nil
1✔
1889
}
1890

1891
func (d *Deployments) LookupDeployment(ctx context.Context,
1892
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
1893
        list, totalCount, err := d.db.FindDeployments(ctx, query)
1✔
1894

1✔
1895
        if err != nil {
2✔
1896
                return nil, 0, errors.Wrap(err, "searching for deployments")
1✔
1897
        }
1✔
1898

1899
        if list == nil {
2✔
1900
                return make([]*model.Deployment, 0), 0, nil
1✔
1901
        }
1✔
1902

1903
        for _, deployment := range list {
2✔
1904
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1905
                        return nil, 0, err
×
1906
                }
×
1907
        }
1908

1909
        return list, totalCount, nil
1✔
1910
}
1911

1912
// SaveDeviceDeploymentLog will save the deployment log for device of
1913
// ID `deviceID`. Returns nil if log was saved successfully.
1914
func (d *Deployments) SaveDeviceDeploymentLog(ctx context.Context, deviceID string,
UNCOV
1915
        deploymentID string, logs []model.LogMessage) error {
×
UNCOV
1916

×
UNCOV
1917
        // repack to temporary deployment log and validate
×
UNCOV
1918
        dlog := model.DeploymentLog{
×
UNCOV
1919
                DeviceID:     deviceID,
×
UNCOV
1920
                DeploymentID: deploymentID,
×
UNCOV
1921
                Messages:     logs,
×
UNCOV
1922
        }
×
UNCOV
1923
        if err := dlog.Validate(); err != nil {
×
1924
                return errors.Wrap(err, ErrStorageInvalidLog.Error())
×
1925
        }
×
1926

UNCOV
1927
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
×
1928
                if err != nil {
×
1929
                        return err
×
1930
                } else {
×
1931
                        return ErrModelDeploymentNotFound
×
1932
                }
×
1933
        }
1934

UNCOV
1935
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
×
1936
                return err
×
1937
        }
×
1938

UNCOV
1939
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
×
UNCOV
1940
                deviceID, deploymentID, true)
×
1941
}
1942

1943
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
UNCOV
1944
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
×
UNCOV
1945

×
UNCOV
1946
        return d.db.GetDeviceDeploymentLog(ctx,
×
UNCOV
1947
                deviceID, deploymentID)
×
UNCOV
1948
}
×
1949

1950
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
UNCOV
1951
        deploymentID string, deviceID string) (bool, error) {
×
UNCOV
1952
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
×
UNCOV
1953
}
×
1954

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

1✔
1958
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
2✔
1959
                return err
1✔
1960
        }
1✔
1961

1962
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
1✔
1963
                ctx, deploymentID)
1✔
1964
        if err != nil {
2✔
1965
                return err
1✔
1966
        }
1✔
1967

1968
        // update statistics
1969
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
2✔
1970
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1971
        }
1✔
1972

1973
        // when aborting the deployment we need to set status directly instead of
1974
        // using recalcDeploymentStatus method;
1975
        // it is possible that the deployment does not have any device deployments yet;
1976
        // in that case, all statistics are 0 and calculating status based on statistics
1977
        // will not work - the calculated status will be "pending"
1978
        if err := d.db.SetDeploymentStatus(ctx,
1✔
1979
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
1✔
1980
                return errors.Wrap(err, "failed to update deployment status")
×
1981
        }
×
1982

1983
        return nil
1✔
1984
}
1985

1986
func (d *Deployments) updateDeviceDeploymentsStatus(
1987
        ctx context.Context,
1988
        deviceId string,
1989
        status model.DeviceDeploymentStatus,
1990
) error {
1✔
1991
        var latestDeployment *time.Time
1✔
1992
        // Retrieve active device deployment for the device
1✔
1993
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
1✔
1994
        if err != nil {
2✔
1995
                return errors.Wrap(err, "Searching for active deployment for the device")
1✔
1996
        } else if deviceDeployment != nil {
3✔
1997
                now := time.Now()
1✔
1998
                ddStatus := model.DeviceDeploymentState{
1✔
1999
                        Status:     status,
1✔
2000
                        FinishTime: &now,
1✔
2001
                }
1✔
2002
                if err := d.updateDeviceDeploymentStatus(
1✔
2003
                        ctx, deviceDeployment, ddStatus,
1✔
2004
                ); err != nil {
1✔
2005
                        return errors.Wrap(err, "updating device deployment status")
×
2006
                }
×
2007
                latestDeployment = deviceDeployment.Created
1✔
2008
        } else {
1✔
2009
                // get latest device deployment for the device
1✔
2010
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
1✔
2011
                if err != nil {
1✔
2012
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
2013
                } else if deviceDeployment == nil {
2✔
2014
                        latestDeployment = &time.Time{}
1✔
2015
                } else {
2✔
2016
                        latestDeployment = deviceDeployment.Created
1✔
2017
                }
1✔
2018
        }
2019

2020
        // get deployments newer then last device deployment
2021
        // iterate over deployments and check if the device is part of the deployment or not
2022
        // if the device is part of the deployment create new, decommisioned device deployment
2023
        var deploy *model.Deployment
1✔
2024
        deploy, err = d.db.FindNewerActiveDeployment(ctx, latestDeployment, deviceId)
1✔
2025
        if err != nil {
1✔
2026
                return errors.Wrap(err, "Failed to search for newer active deployments")
×
2027
        }
×
2028
        if deploy != nil {
2✔
2029
                deviceDeployment, err = d.createDeviceDeploymentWithStatus(ctx,
1✔
2030
                        deviceId, deploy, status)
1✔
2031
                if err != nil {
1✔
2032
                        return err
×
2033
                }
×
2034
                if !status.Active() {
2✔
2035
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
2036
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
2037
                                l := log.FromContext(ctx)
×
2038
                                l.Warn(errors.Wrap(err, "failed to trigger a deployment reindex"))
×
2039
                        }
×
2040
                }
2041
        }
2042

2043
        if err := d.reindexDevice(ctx, deviceId); err != nil {
1✔
2044
                l := log.FromContext(ctx)
×
2045
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
2046
        }
×
2047

2048
        return nil
1✔
2049
}
2050

2051
// DecommissionDevice updates the status of all the pending and active deployments for a device
2052
// to decommissioned
2053
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
1✔
2054
        return d.updateDeviceDeploymentsStatus(
1✔
2055
                ctx,
1✔
2056
                deviceId,
1✔
2057
                model.DeviceDeploymentStatusDecommissioned,
1✔
2058
        )
1✔
2059
}
1✔
2060

2061
// AbortDeviceDeployments aborts all the pending and active deployments for a device
2062
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
1✔
2063
        return d.updateDeviceDeploymentsStatus(
1✔
2064
                ctx,
1✔
2065
                deviceId,
1✔
2066
                model.DeviceDeploymentStatusAborted,
1✔
2067
        )
1✔
2068
}
1✔
2069

2070
// DeleteDeviceDeploymentsHistory deletes the device deployments history
2071
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
1✔
2072
        // get device deployments which will be marked as deleted
1✔
2073
        f := false
1✔
2074
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
1✔
2075
        if err != nil {
1✔
2076
                return err
×
2077
        }
×
2078

2079
        // no device deployments to update
2080
        if len(dd) <= 0 {
1✔
2081
                return nil
×
2082
        }
×
2083

2084
        // mark device deployments as deleted
2085
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
2✔
2086
                return err
1✔
2087
        }
1✔
2088

2089
        // trigger reindexing of updated device deployments
2090
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2091
        for i, d := range dd {
2✔
2092
                deviceDeployments[i].ID = d.Id
1✔
2093
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2094
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2095
        }
1✔
2096
        if d.reportingClient != nil {
2✔
2097
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2098
        }
1✔
2099
        return err
1✔
2100
}
2101

2102
// Storage settings
2103
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
1✔
2104
        settings, err := d.db.GetStorageSettings(ctx)
1✔
2105
        if err != nil {
2✔
2106
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2107
        }
1✔
2108

2109
        return settings, nil
1✔
2110
}
2111

2112
func (d *Deployments) SetStorageSettings(
2113
        ctx context.Context,
2114
        storageSettings *model.StorageSettings,
2115
) error {
1✔
2116
        if storageSettings != nil {
2✔
2117
                ctx = storage.SettingsWithContext(ctx, storageSettings)
1✔
2118
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
1✔
2119
                        return errors.WithMessage(err,
×
2120
                                "the provided storage settings failed the health check",
×
2121
                        )
×
2122
                }
×
2123
        }
2124
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
2✔
2125
                return errors.Wrap(err, "Failed to save settings")
1✔
2126
        }
1✔
2127

2128
        return nil
1✔
2129
}
2130

2131
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
1✔
2132
        d.reportingClient = c
1✔
2133
        return d
1✔
2134
}
1✔
2135

2136
func (d *Deployments) haveReporting() bool {
1✔
2137
        return d.reportingClient != nil
1✔
2138
}
1✔
2139

2140
func (d *Deployments) search(
2141
        ctx context.Context,
2142
        tid string,
2143
        parms model.SearchParams,
2144
) ([]model.InvDevice, int, error) {
1✔
2145
        if d.haveReporting() {
2✔
2146
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2147
        } else {
2✔
2148
                return d.inventoryClient.Search(ctx, tid, parms)
1✔
2149
        }
1✔
2150
}
2151

2152
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2153
        ctx context.Context,
2154
        artifactName string,
2155
) error {
1✔
2156
        // first check if there are pending deployments with given artifact name
1✔
2157
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
1✔
2158
        if err != nil {
1✔
2159
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
2160
        }
×
2161
        if !exists {
1✔
UNCOV
2162
                return nil
×
UNCOV
2163
        }
×
2164

2165
        // Assign artifacts to the deployments with given artifact name
2166
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2167
        if err != nil {
1✔
2168
                return errors.Wrap(err, "Finding artifact with given name")
×
2169
        }
×
2170

2171
        if len(artifacts) == 0 {
1✔
2172
                return ErrNoArtifact
×
2173
        }
×
2174
        artifactIDs := getArtifactIDs(artifacts)
1✔
2175
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2176
}
2177

2178
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
1✔
2179
        if d.reportingClient != nil {
2✔
2180
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
1✔
2181
        }
1✔
2182
        return nil
1✔
2183
}
2184

2185
func (d *Deployments) reindexDeployment(ctx context.Context,
2186
        deviceID, deploymentID, ID string) error {
1✔
2187
        if d.reportingClient != nil {
2✔
2188
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
1✔
2189
        }
1✔
2190
        return nil
1✔
2191
}
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