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

mendersoftware / deployments / 1376315855

04 Jul 2024 10:22AM UTC coverage: 79.694% (-0.06%) from 79.752%
1376315855

push

gitlab-ci

web-flow
Merge pull request #1029 from alfrunes/perf-deployment-status

perf: Optimize database interactions when updating deployment stats

59 of 80 new or added lines in 3 files covered. (73.75%)

711 existing lines in 10 files now uncovered.

8124 of 10194 relevant lines covered (79.69%)

34.59 hits per line

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

77.45
/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/go-lib-micro/identity"
31
        "github.com/mendersoftware/go-lib-micro/log"
32
        "github.com/mendersoftware/mender-artifact/areader"
33
        "github.com/mendersoftware/mender-artifact/artifact"
34
        "github.com/mendersoftware/mender-artifact/awriter"
35
        "github.com/mendersoftware/mender-artifact/handlers"
36

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

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

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

61
        fileSuffixTmp = ".tmp"
62

63
        inprogressIdleTime = time.Hour
64
)
65

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

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

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

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

109
//deployments
110

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

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

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

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

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

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

215
// Compile-time check
216
var _ App = &Deployments{}
217

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

232
func (d *Deployments) SetWorkflowsClient(workflowsClient workflows.Client) {
4✔
233
        d.workflowsClient = workflowsClient
4✔
234
}
4✔
235

236
func (d *Deployments) SetInventoryClient(inventoryClient inventory.Client) {
9✔
237
        d.inventoryClient = inventoryClient
9✔
238
}
9✔
239

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

253
        err = d.workflowsClient.CheckHealth(ctx)
4✔
254
        if err != nil {
5✔
255
                return errors.Wrap(err, "Workflows service unhealthy")
1✔
256
        }
1✔
257

258
        err = d.inventoryClient.CheckHealth(ctx)
3✔
259
        if err != nil {
4✔
260
                return errors.Wrap(err, "Inventory service unhealthy")
1✔
261
        }
1✔
262

263
        if d.reportingClient != nil {
4✔
264
                err = d.reportingClient.CheckHealth(ctx)
2✔
265
                if err != nil {
3✔
266
                        return errors.Wrap(err, "Reporting service unhealthy")
1✔
267
                }
1✔
268
        }
269
        return nil
1✔
270
}
271

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

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

1✔
306
        } else if err != nil {
4✔
307
                return nil, errors.Wrap(err, "failed to obtain limit from storage")
1✔
308
        }
1✔
309
        return limit, nil
1✔
310
}
311

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

317
        return nil
2✔
318
}
319

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

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

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

5✔
359
        l := log.FromContext(ctx)
5✔
360
        ctx, err := d.contextWithStorageSettings(ctx)
5✔
361
        if err != nil {
5✔
362
                return "", err
×
363
        }
×
364

365
        // create pipe
366
        pR, pW := io.Pipe()
5✔
367

5✔
368
        artifactReader := utils.CountReads(multipartUploadMsg.ArtifactReader)
5✔
369

5✔
370
        tee := io.TeeReader(artifactReader, pW)
5✔
371

5✔
372
        uid, err := uuid.Parse(multipartUploadMsg.ArtifactID)
5✔
373
        if err != nil {
6✔
UNCOV
374
                uid, _ = uuid.NewRandom()
1✔
UNCOV
375
        }
1✔
376
        artifactID := uid.String()
5✔
377

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

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

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

437
        // close the pipe
UNCOV
438
        pW.Close()
1✔
UNCOV
439

1✔
UNCOV
440
        // collect output from the goroutine
1✔
UNCOV
441
        if uploadResponseErr := <-ch; uploadResponseErr != nil {
1✔
442
                return artifactID, uploadResponseErr
×
443
        }
×
444

UNCOV
445
        size := artifactReader.Count()
1✔
UNCOV
446
        if skipVerify && validMetadata {
2✔
UNCOV
447
                size = metadata.Size
1✔
UNCOV
448
        }
1✔
UNCOV
449
        image := model.NewImage(
1✔
UNCOV
450
                artifactID,
1✔
UNCOV
451
                multipartUploadMsg.MetaConstructor,
1✔
UNCOV
452
                metaArtifactConstructor,
1✔
UNCOV
453
                size,
1✔
UNCOV
454
        )
1✔
UNCOV
455

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

1✔
UNCOV
474
        // update release
1✔
UNCOV
475
        if err := d.updateRelease(ctx, image, nil); err != nil {
1✔
476
                return "", err
×
477
        }
×
478

UNCOV
479
        if err := d.UpdateDeploymentsWithArtifactName(ctx, metaArtifactConstructor.Name); err != nil {
1✔
480
                return "", errors.Wrap(err, "fail to update deployments")
×
481
        }
×
482

UNCOV
483
        return artifactID, nil
1✔
484
}
485

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

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

11✔
509
        if multipartGenerateImageMsg == nil {
12✔
510
                return "", ErrModelMultipartUploadMsgMalformed
1✔
511
        }
1✔
512

513
        imgPath, err := d.handleRawFile(ctx, multipartGenerateImageMsg)
10✔
514
        if err != nil {
15✔
515
                return "", err
5✔
516
        }
5✔
517
        if id := identity.FromContext(ctx); id != nil && len(id.Tenant) > 0 {
6✔
518
                multipartGenerateImageMsg.TenantID = id.Tenant
1✔
519
        }
1✔
520
        err = d.workflowsClient.StartGenerateArtifact(ctx, multipartGenerateImageMsg)
5✔
521
        if err != nil {
7✔
522
                if cleanupErr := d.objectStorage.DeleteObject(ctx, imgPath); cleanupErr != nil {
3✔
523
                        return "", errors.Wrap(err, cleanupErr.Error())
1✔
524
                }
1✔
525
                return "", err
1✔
526
        }
527

528
        return multipartGenerateImageMsg.ArtifactID, err
3✔
529
}
530

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

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

2✔
574
        return &buf, err
2✔
575
}
576

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

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

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

622
        link, err := d.objectStorage.GetRequest(
7✔
623
                ctx,
7✔
624
                filePath,
7✔
625
                path.Base(filePath),
7✔
626
                DefaultImageGenerationLinkExpire,
7✔
627
        )
7✔
628
        if err != nil {
8✔
629
                return "", err
1✔
630
        }
1✔
631
        multipartMsg.GetArtifactURI = link.Uri
6✔
632

6✔
633
        link, err = d.objectStorage.DeleteRequest(ctx, filePath, DefaultImageGenerationLinkExpire)
6✔
634
        if err != nil {
7✔
635
                return "", err
1✔
636
        }
1✔
637
        multipartMsg.DeleteArtifactURI = link.Uri
5✔
638

5✔
639
        return artifactID, nil
5✔
640
}
641

642
// GetImage allows to fetch image object with specified id
643
// Nil if not found
UNCOV
644
func (d *Deployments) GetImage(ctx context.Context, id string) (*model.Image, error) {
1✔
UNCOV
645

1✔
UNCOV
646
        image, err := d.db.FindImageByID(ctx, id)
1✔
UNCOV
647
        if err != nil {
1✔
648
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
649
        }
×
650

UNCOV
651
        if image == nil {
2✔
UNCOV
652
                return nil, nil
1✔
UNCOV
653
        }
1✔
654

UNCOV
655
        return image, nil
1✔
656
}
657

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

1✔
UNCOV
668
        if err != nil {
1✔
669
                return errors.Wrap(err, "Getting image metadata")
×
670
        }
×
671

UNCOV
672
        if found == nil {
1✔
673
                return ErrImageMetaNotFound
×
674
        }
×
675

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

681
        // Image is in use, not allowed to delete
UNCOV
682
        if inUse {
2✔
UNCOV
683
                return ErrModelImageInActiveDeployment
1✔
UNCOV
684
        }
1✔
685

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

697
        // Delete metadata
UNCOV
698
        if err := d.db.DeleteImage(ctx, imageID); err != nil {
1✔
699
                return errors.Wrap(err, "Deleting image metadata")
×
700
        }
×
701

702
        // update release
UNCOV
703
        if err := d.updateRelease(ctx, nil, found); err != nil {
1✔
704
                return err
×
705
        }
×
706

UNCOV
707
        return nil
1✔
708
}
709

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

UNCOV
720
        if imageList == nil {
2✔
UNCOV
721
                return make([]*model.Image, 0), 0, nil
1✔
UNCOV
722
        }
1✔
723

UNCOV
724
        return imageList, count, nil
1✔
725
}
726

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

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

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

740
        if found {
×
741
                return false, ErrModelImageUsedInAnyDeployment
×
742
        }
×
743

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

749
        if foundImage == nil {
×
750
                return false, nil
×
751
        }
×
752

753
        foundImage.SetModified(time.Now())
×
754
        foundImage.ImageMeta = constructor
×
755

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

761
        if err := d.updateReleaseEditArtifact(ctx, foundImage); err != nil {
×
762
                return false, err
×
763
        }
×
764

765
        return true, nil
×
766
}
767

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

1✔
UNCOV
773
        image, err := d.GetImage(ctx, imageID)
1✔
UNCOV
774
        if err != nil {
1✔
775
                return nil, errors.Wrap(err, "Searching for image with specified ID")
×
776
        }
×
777

UNCOV
778
        if image == nil {
1✔
779
                return nil, nil
×
780
        }
×
781

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

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

UNCOV
802
        return link, nil
1✔
803
}
804

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

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

834
        return upLink, err
3✔
835
}
836

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

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

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

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

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

UNCOV
955
func getArtifactInfo(info artifact.Info) *model.ArtifactInfo {
1✔
UNCOV
956
        return &model.ArtifactInfo{
1✔
UNCOV
957
                Format:  info.Format,
1✔
UNCOV
958
                Version: uint(info.Version),
1✔
UNCOV
959
        }
1✔
UNCOV
960
}
1✔
961

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

975
func getMetaFromArchive(r *io.Reader, skipVerify bool) (*model.ArtifactMeta, error) {
5✔
976
        metaArtifact := model.NewArtifactMeta()
5✔
977

5✔
978
        aReader := areader.NewReader(*r)
5✔
979

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

987
        var err error
5✔
988
        if skipVerify {
8✔
989
                err = aReader.ReadArtifactHeaders()
3✔
990
                if err != nil {
5✔
991
                        return nil, errors.Wrap(err, "reading artifact error")
2✔
992
                }
2✔
993
        } else {
3✔
994
                err = aReader.ReadArtifact()
3✔
995
                if err != nil {
6✔
996
                        return nil, errors.Wrap(err, "reading artifact error")
3✔
997
                }
3✔
998
        }
999

UNCOV
1000
        metaArtifact.Info = getArtifactInfo(aReader.GetInfo())
1✔
UNCOV
1001
        metaArtifact.DeviceTypesCompatible = aReader.GetCompatibleDevices()
1✔
UNCOV
1002

1✔
UNCOV
1003
        metaArtifact.Name = aReader.GetArtifactName()
1✔
UNCOV
1004
        if metaArtifact.Info.Version == 3 {
2✔
UNCOV
1005
                metaArtifact.Depends, err = aReader.MergeArtifactDepends()
1✔
UNCOV
1006
                if err != nil {
1✔
1007
                        return nil, errors.Wrap(err,
×
1008
                                "error parsing version 3 artifact")
×
1009
                }
×
1010

UNCOV
1011
                metaArtifact.Provides, err = aReader.MergeArtifactProvides()
1✔
UNCOV
1012
                if err != nil {
1✔
1013
                        return nil, errors.Wrap(err,
×
1014
                                "error parsing version 3 artifact")
×
1015
                }
×
1016

UNCOV
1017
                metaArtifact.ClearsProvides = aReader.MergeArtifactClearsProvides()
1✔
1018
        }
1019

UNCOV
1020
        for _, p := range aReader.GetHandlers() {
2✔
UNCOV
1021
                uFiles, err := getUpdateFiles(p.GetUpdateFiles())
1✔
UNCOV
1022
                if err != nil {
1✔
1023
                        return nil, errors.Wrap(err, "Cannot get update files:")
×
1024
                }
×
1025

UNCOV
1026
                uMetadata, err := p.GetUpdateMetaData()
1✔
UNCOV
1027
                if err != nil {
1✔
1028
                        return nil, errors.Wrap(err, "Cannot get update metadata")
×
1029
                }
×
1030

UNCOV
1031
                metaArtifact.Updates = append(
1✔
UNCOV
1032
                        metaArtifact.Updates,
1✔
UNCOV
1033
                        model.Update{
1✔
UNCOV
1034
                                TypeInfo: model.ArtifactUpdateTypeInfo{
1✔
UNCOV
1035
                                        Type: p.GetUpdateType(),
1✔
UNCOV
1036
                                },
1✔
UNCOV
1037
                                Files:    uFiles,
1✔
UNCOV
1038
                                MetaData: uMetadata,
1✔
UNCOV
1039
                        })
1✔
1040
        }
1041

UNCOV
1042
        return metaArtifact, nil
1✔
1043
}
1044

1045
func getArtifactIDs(artifacts []*model.Image) []string {
8✔
1046
        artifactIDs := make([]string, 0, len(artifacts))
8✔
1047
        for _, artifact := range artifacts {
16✔
1048
                artifactIDs = append(artifactIDs, artifact.Id)
8✔
1049
        }
8✔
1050
        return artifactIDs
8✔
1051
}
1052

1053
// deployments
1054
func inventoryDevicesToDevicesIds(devices []model.InvDevice) []string {
4✔
1055
        ids := make([]string, len(devices))
4✔
1056
        for i, d := range devices {
8✔
1057
                ids[i] = d.ID
4✔
1058
        }
4✔
1059

1060
        return ids
4✔
1061
}
1062

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

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

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

1116
        return constructor, nil
3✔
1117
}
1118

1119
// CreateDeviceConfigurationDeployment creates new configuration deployment for the device.
1120
func (d *Deployments) CreateDeviceConfigurationDeployment(
1121
        ctx context.Context, constructor *model.ConfigurationDeploymentConstructor,
1122
        deviceID, deploymentID string) (string, error) {
5✔
1123

5✔
1124
        if constructor == nil {
6✔
1125
                return "", ErrModelMissingInput
1✔
1126
        }
1✔
1127

1128
        deployment, err := model.NewDeploymentFromConfigurationDeploymentConstructor(
4✔
1129
                constructor,
4✔
1130
                deploymentID,
4✔
1131
        )
4✔
1132
        if err != nil {
4✔
1133
                return "", errors.Wrap(err, "failed to create deployment")
×
1134
        }
×
1135

1136
        deployment.DeviceList = []string{deviceID}
4✔
1137
        deployment.MaxDevices = 1
4✔
1138
        deployment.Configuration = []byte(constructor.Configuration)
4✔
1139
        deployment.Type = model.DeploymentTypeConfiguration
4✔
1140

4✔
1141
        groups, err := d.getDeploymentGroups(ctx, []string{deviceID})
4✔
1142
        if err != nil {
5✔
1143
                return "", err
1✔
1144
        }
1✔
1145
        deployment.Groups = groups
3✔
1146

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

1157
        return deployment.Id, nil
2✔
1158
}
1159

1160
// CreateDeployment precomputes new deployment and schedules it for devices.
1161
func (d *Deployments) CreateDeployment(ctx context.Context,
1162
        constructor *model.DeploymentConstructor) (string, error) {
10✔
1163

10✔
1164
        var err error
10✔
1165

10✔
1166
        if constructor == nil {
11✔
1167
                return "", ErrModelMissingInput
1✔
1168
        }
1✔
1169

1170
        if err := constructor.Validate(); err != nil {
9✔
1171
                return "", errors.Wrap(err, "Validating deployment")
×
1172
        }
×
1173

1174
        if len(constructor.Group) > 0 || constructor.AllDevices {
14✔
1175
                constructor, err = d.updateDeploymentConstructor(ctx, constructor)
5✔
1176
                if err != nil {
7✔
1177
                        return "", err
2✔
1178
                }
2✔
1179
        }
1180

1181
        deployment, err := model.NewDeploymentFromConstructor(constructor)
7✔
1182
        if err != nil {
7✔
1183
                return "", errors.Wrap(err, "failed to create deployment")
×
1184
        }
×
1185

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

1194
        if len(artifacts) == 0 {
8✔
UNCOV
1195
                return "", ErrNoArtifact
1✔
UNCOV
1196
        }
1✔
1197

1198
        deployment.Artifacts = getArtifactIDs(artifacts)
7✔
1199
        deployment.DeviceList = constructor.Devices
7✔
1200
        deployment.MaxDevices = len(constructor.Devices)
7✔
1201
        deployment.Type = model.DeploymentTypeSoftware
7✔
1202
        if len(constructor.Group) > 0 {
10✔
1203
                deployment.Groups = []string{constructor.Group}
3✔
1204
        }
3✔
1205

1206
        // single device deployment case
1207
        if len(deployment.Groups) == 0 && len(constructor.Devices) == 1 {
11✔
1208
                groups, err := d.getDeploymentGroups(ctx, constructor.Devices)
4✔
1209
                if err != nil {
4✔
1210
                        return "", err
×
1211
                }
×
1212
                deployment.Groups = groups
4✔
1213
        }
1214

1215
        if err := d.db.InsertDeployment(ctx, deployment); err != nil {
10✔
1216
                if err == mongo.ErrConflictingDeployment {
5✔
1217
                        return "", ErrConflictingDeployment
2✔
1218
                }
2✔
1219
                return "", errors.Wrap(err, "Storing deployment data")
1✔
1220
        }
1221

1222
        return deployment.Id, nil
5✔
1223
}
1224

1225
func (d *Deployments) getDeploymentGroups(
1226
        ctx context.Context,
1227
        devices []string,
1228
) ([]string, error) {
7✔
1229
        id := identity.FromContext(ctx)
7✔
1230

7✔
1231
        //only for single device deployment case
7✔
1232
        if len(devices) != 1 {
7✔
1233
                return nil, nil
×
1234
        }
×
1235

1236
        if id == nil {
8✔
UNCOV
1237
                id = &identity.Identity{}
1✔
UNCOV
1238
        }
1✔
1239

1240
        groups, err := d.inventoryClient.GetDeviceGroups(ctx, id.Tenant, devices[0])
7✔
1241
        if err != nil && err != inventory.ErrDevNotFound {
8✔
1242
                return nil, err
1✔
1243
        }
1✔
1244
        return groups, nil
6✔
1245
}
1246

1247
// IsDeploymentFinished checks if there is unfinished deployment with given ID
1248
func (d *Deployments) IsDeploymentFinished(
1249
        ctx context.Context,
1250
        deploymentID string,
UNCOV
1251
) (bool, error) {
1✔
UNCOV
1252
        deployment, err := d.db.FindUnfinishedByID(ctx, deploymentID)
1✔
UNCOV
1253
        if err != nil {
1✔
1254
                return false, errors.Wrap(err, "Searching for unfinished deployment by ID")
×
1255
        }
×
UNCOV
1256
        if deployment == nil {
2✔
UNCOV
1257
                return true, nil
1✔
UNCOV
1258
        }
1✔
1259

UNCOV
1260
        return false, nil
1✔
1261
}
1262

1263
// GetDeployment fetches deployment by ID
1264
func (d *Deployments) GetDeployment(ctx context.Context,
UNCOV
1265
        deploymentID string) (*model.Deployment, error) {
1✔
UNCOV
1266

1✔
UNCOV
1267
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
UNCOV
1268
        if err != nil {
1✔
1269
                return nil, errors.Wrap(err, "Searching for deployment by ID")
×
1270
        }
×
1271

UNCOV
1272
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
1✔
1273
                return nil, err
×
1274
        }
×
1275

UNCOV
1276
        return deployment, nil
1✔
1277
}
1278

1279
// ImageUsedInActiveDeployment checks if specified image is in use by deployments. Image is
1280
// considered to be in use if it's participating in at lest one non success/error deployment.
1281
func (d *Deployments) ImageUsedInActiveDeployment(ctx context.Context,
1282
        imageID string) (bool, error) {
3✔
1283

3✔
1284
        var found bool
3✔
1285

3✔
1286
        found, err := d.db.ExistUnfinishedByArtifactId(ctx, imageID)
3✔
1287
        if err != nil {
4✔
1288
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
1✔
1289
        }
1✔
1290

1291
        return found, nil
2✔
1292
}
1293

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

×
1298
        var found bool
×
1299

×
1300
        found, err := d.db.ExistByArtifactId(ctx, imageID)
×
1301
        if err != nil {
×
1302
                return false, errors.Wrap(err, "Checking if image is used by active deployment")
×
1303
        }
×
1304

1305
        return found, nil
×
1306
}
1307

1308
// Retrieves the model.Deployment and model.DeviceDeployment structures
1309
// for the device. Upon error, nil is returned for both deployment structures.
1310
func (d *Deployments) getDeploymentForDevice(ctx context.Context,
1311
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
2✔
1312

2✔
1313
        // Retrieve device deployment
2✔
1314
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceID)
2✔
1315

2✔
1316
        if err != nil {
2✔
1317
                return nil, nil, errors.Wrap(err,
×
1318
                        "Searching for oldest active deployment for the device")
×
1319
        } else if deviceDeployment == nil {
3✔
UNCOV
1320
                return d.getNewDeploymentForDevice(ctx, deviceID)
1✔
UNCOV
1321
        }
1✔
1322

1323
        deployment, err := d.db.FindDeploymentByID(ctx, deviceDeployment.DeploymentId)
2✔
1324
        if err != nil {
2✔
1325
                return nil, nil, errors.Wrap(err, "checking deployment id")
×
1326
        }
×
1327
        if deployment == nil {
2✔
1328
                return nil, nil, errors.New("No deployment corresponding to device deployment")
×
1329
        }
×
1330

1331
        return deployment, deviceDeployment, nil
2✔
1332
}
1333

1334
// getNewDeploymentForDevice returns deployment object and creates and returns
1335
// new device deployment for the device;
1336
//
1337
// we are interested only in the deployments that are newer than the latest
1338
// deployment applied by the device;
1339
// this way we guarantee that the device will not receive deployment
1340
// that is older than the one installed on the device;
1341
func (d *Deployments) getNewDeploymentForDevice(ctx context.Context,
UNCOV
1342
        deviceID string) (*model.Deployment, *model.DeviceDeployment, error) {
1✔
UNCOV
1343

1✔
UNCOV
1344
        var lastDeployment *time.Time
1✔
UNCOV
1345
        //get latest device deployment for the device;
1✔
UNCOV
1346
        deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceID)
1✔
UNCOV
1347
        if err != nil {
1✔
1348
                return nil, nil, errors.Wrap(err,
×
1349
                        "Searching for latest active deployment for the device")
×
UNCOV
1350
        } else if deviceDeployment == nil {
2✔
UNCOV
1351
                lastDeployment = &time.Time{}
1✔
UNCOV
1352
        } else {
2✔
UNCOV
1353
                lastDeployment = deviceDeployment.Created
1✔
UNCOV
1354
        }
1✔
1355

1356
        //get deployments newer then last device deployment
1357
        //iterate over deployments and check if the device is part of the deployment or not
UNCOV
1358
        var deploy *model.Deployment
1✔
UNCOV
1359
        deploy, err = d.db.FindNewerActiveDeployment(ctx, lastDeployment, deviceID)
1✔
UNCOV
1360
        if err != nil {
1✔
1361
                return nil, nil, errors.Wrap(err, "Failed to search for newer active deployments")
×
1362
        }
×
UNCOV
1363
        if deploy != nil {
2✔
UNCOV
1364
                deviceDeployment, err := d.createDeviceDeploymentWithStatus(ctx,
1✔
UNCOV
1365
                        deviceID, deploy, model.DeviceDeploymentStatusPending)
1✔
UNCOV
1366
                if err != nil {
1✔
1367
                        return nil, nil, err
×
1368
                }
×
UNCOV
1369
                return deploy, deviceDeployment, nil
1✔
1370
        }
UNCOV
1371
        return nil, nil, nil
1✔
1372
}
1373

1374
func (d *Deployments) createDeviceDeploymentWithStatus(
1375
        ctx context.Context, deviceID string,
1376
        deployment *model.Deployment, status model.DeviceDeploymentStatus,
1377
) (*model.DeviceDeployment, error) {
6✔
1378
        prevStatus := model.DeviceDeploymentStatusNull
6✔
1379
        deviceDeployment, err := d.db.GetDeviceDeployment(ctx, deployment.Id, deviceID, true)
6✔
1380
        if err != nil && err != mongo.ErrStorageNotFound {
6✔
1381
                return nil, err
×
1382
        } else if deviceDeployment != nil {
6✔
1383
                prevStatus = deviceDeployment.Status
×
1384
        }
×
1385

1386
        deviceDeployment = model.NewDeviceDeployment(deviceID, deployment.Id)
6✔
1387
        deviceDeployment.Status = status
6✔
1388
        deviceDeployment.Active = status.Active()
6✔
1389
        deviceDeployment.Created = deployment.Created
6✔
1390

6✔
1391
        if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
6✔
1392
                return nil, err
×
1393
        }
×
1394

1395
        if err := d.db.InsertDeviceDeployment(ctx, deviceDeployment,
6✔
1396
                prevStatus == model.DeviceDeploymentStatusNull); err != nil {
6✔
1397
                return nil, err
×
1398
        }
×
1399

1400
        if prevStatus != status {
12✔
1401
                beforeStatus := deployment.GetStatus()
6✔
1402
                // after inserting new device deployment update deployment stats
6✔
1403
                // in the database, and update deployment status
6✔
1404
                deployment.Stats, err = d.db.UpdateStatsInc(
6✔
1405
                        ctx, deployment.Id,
6✔
1406
                        prevStatus, status,
6✔
1407
                )
6✔
1408
                if err != nil {
6✔
NEW
1409
                        return nil, err
×
NEW
1410
                }
×
1411
                newStatus := deployment.GetStatus()
6✔
1412
                if beforeStatus != newStatus {
6✔
NEW
1413
                        err = d.db.SetDeploymentStatus(
×
NEW
1414
                                ctx, deployment.Id,
×
NEW
1415
                                newStatus, time.Now(),
×
NEW
1416
                        )
×
NEW
1417
                        if err != nil {
×
NEW
1418
                                return nil, errors.Wrap(err,
×
NEW
1419
                                        "failed to update deployment status")
×
NEW
1420
                        }
×
1421
                }
1422
        }
1423

1424
        if !status.Active() {
11✔
1425
                err := d.reindexDevice(ctx, deviceID)
5✔
1426
                if err != nil {
5✔
1427
                        l := log.FromContext(ctx)
×
1428
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1429
                }
×
1430
                if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1431
                        deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1432
                        l := log.FromContext(ctx)
×
1433
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1434
                }
×
1435
        }
1436

1437
        return deviceDeployment, nil
6✔
1438
}
1439

1440
// GetDeploymentForDeviceWithCurrent returns deployment for the device
1441
func (d *Deployments) GetDeploymentForDeviceWithCurrent(ctx context.Context, deviceID string,
1442
        request *model.DeploymentNextRequest) (*model.DeploymentInstructions, error) {
2✔
1443

2✔
1444
        deployment, deviceDeployment, err := d.getDeploymentForDevice(ctx, deviceID)
2✔
1445
        if err != nil {
2✔
1446
                return nil, ErrModelInternal
×
1447
        } else if deployment == nil {
3✔
UNCOV
1448
                return nil, nil
1✔
UNCOV
1449
        }
1✔
1450

1451
        err = d.saveDeviceDeploymentRequest(ctx, deviceID, deviceDeployment, request)
2✔
1452
        if err != nil {
3✔
UNCOV
1453
                return nil, err
1✔
UNCOV
1454
        }
1✔
1455
        return d.getDeploymentInstructions(ctx, deployment, deviceDeployment, request)
2✔
1456
}
1457

1458
func (d *Deployments) getDeploymentInstructions(
1459
        ctx context.Context,
1460
        deployment *model.Deployment,
1461
        deviceDeployment *model.DeviceDeployment,
1462
        request *model.DeploymentNextRequest,
1463
) (*model.DeploymentInstructions, error) {
2✔
1464

2✔
1465
        var newArtifactAssigned bool
2✔
1466

2✔
1467
        l := log.FromContext(ctx)
2✔
1468

2✔
1469
        if deployment.Type == model.DeploymentTypeConfiguration {
3✔
UNCOV
1470
                // There's nothing more we need to do, the link must be filled
1✔
UNCOV
1471
                // in by the API layer.
1✔
UNCOV
1472
                return &model.DeploymentInstructions{
1✔
UNCOV
1473
                        ID: deployment.Id,
1✔
UNCOV
1474
                        Artifact: model.ArtifactDeploymentInstructions{
1✔
UNCOV
1475
                                // configuration artifacts are created on demand, so they do not have IDs
1✔
UNCOV
1476
                                // use deployment ID togheter with device ID as artifact ID
1✔
UNCOV
1477
                                ID:                    deployment.Id + deviceDeployment.DeviceId,
1✔
UNCOV
1478
                                ArtifactName:          deployment.ArtifactName,
1✔
UNCOV
1479
                                DeviceTypesCompatible: []string{request.DeviceProvides.DeviceType},
1✔
UNCOV
1480
                        },
1✔
UNCOV
1481
                        Type: model.DeploymentTypeConfiguration,
1✔
UNCOV
1482
                }, nil
1✔
UNCOV
1483
        }
1✔
1484

1485
        // assing artifact to the device deployment
1486
        // only if it was not assgined previously
1487
        if deviceDeployment.Image == nil {
4✔
1488
                if err := d.assignArtifact(
2✔
1489
                        ctx, deployment, deviceDeployment, request.DeviceProvides); err != nil {
2✔
1490
                        return nil, err
×
1491
                }
×
1492
                newArtifactAssigned = true
2✔
1493
        }
1494

1495
        if deviceDeployment.Image == nil {
2✔
1496
                // No artifact - return empty response
×
1497
                return nil, nil
×
1498
        }
×
1499

1500
        // if the deployment is not forcing the installation, and
1501
        // if artifact was recognized as already installed, and this is
1502
        // a new device deployment - indicated by device deployment status "pending",
1503
        // handle already installed artifact case
1504
        if !deployment.ForceInstallation &&
2✔
1505
                d.isAlreadyInstalled(request, deviceDeployment) &&
2✔
1506
                deviceDeployment.Status == model.DeviceDeploymentStatusPending {
4✔
1507
                return nil, d.handleAlreadyInstalled(ctx, deviceDeployment)
2✔
1508
        }
2✔
1509

1510
        // if new artifact has been assigned to device deployment
1511
        // add artifact size to deployment total size,
1512
        // before returning deployment instruction to the device
UNCOV
1513
        if newArtifactAssigned {
2✔
UNCOV
1514
                if err := d.db.IncrementDeploymentTotalSize(
1✔
UNCOV
1515
                        ctx, deviceDeployment.DeploymentId, deviceDeployment.Image.Size); err != nil {
1✔
1516
                        l.Errorf("failed to increment deployment total size: %s", err.Error())
×
1517
                }
×
1518
        }
1519

UNCOV
1520
        ctx, err := d.contextWithStorageSettings(ctx)
1✔
UNCOV
1521
        if err != nil {
1✔
1522
                return nil, err
×
1523
        }
×
1524

UNCOV
1525
        imagePath := model.ImagePathFromContext(ctx, deviceDeployment.Image.Id)
1✔
UNCOV
1526
        link, err := d.objectStorage.GetRequest(
1✔
UNCOV
1527
                ctx,
1✔
UNCOV
1528
                imagePath,
1✔
UNCOV
1529
                deviceDeployment.Image.Name+model.ArtifactFileSuffix,
1✔
UNCOV
1530
                DefaultUpdateDownloadLinkExpire,
1✔
UNCOV
1531
        )
1✔
UNCOV
1532
        if err != nil {
1✔
1533
                return nil, errors.Wrap(err, "Generating download link for the device")
×
1534
        }
×
1535

UNCOV
1536
        instructions := &model.DeploymentInstructions{
1✔
UNCOV
1537
                ID: deviceDeployment.DeploymentId,
1✔
UNCOV
1538
                Artifact: model.ArtifactDeploymentInstructions{
1✔
UNCOV
1539
                        ID: deviceDeployment.Image.Id,
1✔
UNCOV
1540
                        ArtifactName: deviceDeployment.Image.
1✔
UNCOV
1541
                                ArtifactMeta.Name,
1✔
UNCOV
1542
                        Source: *link,
1✔
UNCOV
1543
                        DeviceTypesCompatible: deviceDeployment.Image.
1✔
UNCOV
1544
                                ArtifactMeta.DeviceTypesCompatible,
1✔
UNCOV
1545
                },
1✔
UNCOV
1546
        }
1✔
UNCOV
1547

1✔
UNCOV
1548
        return instructions, nil
1✔
1549
}
1550

1551
func (d *Deployments) saveDeviceDeploymentRequest(ctx context.Context, deviceID string,
1552
        deviceDeployment *model.DeviceDeployment, request *model.DeploymentNextRequest) error {
2✔
1553
        if deviceDeployment.Request != nil {
3✔
UNCOV
1554
                if !reflect.DeepEqual(deviceDeployment.Request, request) {
2✔
UNCOV
1555
                        // the device reported different device type and/or artifact name during the
1✔
UNCOV
1556
                        // update process, this can happen if the mender-store DB in the client is not
1✔
UNCOV
1557
                        // persistent so a new deployment start without a previous one is still ongoing;
1✔
UNCOV
1558
                        // mark deployment for this device as failed to force client to rollback
1✔
UNCOV
1559
                        l := log.FromContext(ctx)
1✔
UNCOV
1560
                        l.Errorf(
1✔
UNCOV
1561
                                "Device with id %s reported new data: %s during update process;"+
1✔
UNCOV
1562
                                        "old data: %s",
1✔
UNCOV
1563
                                deviceID, request, deviceDeployment.Request)
1✔
UNCOV
1564

1✔
NEW
1565
                        if err := d.updateDeviceDeploymentStatus(ctx, deviceDeployment,
1✔
UNCOV
1566
                                model.DeviceDeploymentState{
1✔
UNCOV
1567
                                        Status: model.DeviceDeploymentStatusFailure,
1✔
UNCOV
1568
                                }); err != nil {
1✔
1569
                                return errors.Wrap(err, "Failed to update deployment status")
×
1570
                        }
×
UNCOV
1571
                        if err := d.reindexDevice(ctx, deviceDeployment.DeviceId); err != nil {
1✔
1572
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1573
                        }
×
UNCOV
1574
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
1✔
UNCOV
1575
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
1✔
1576
                                l := log.FromContext(ctx)
×
1577
                                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1578
                        }
×
UNCOV
1579
                        return ErrConflictingRequestData
1✔
1580
                }
1581
        } else {
2✔
1582
                // save the request
2✔
1583
                if err := d.db.SaveDeviceDeploymentRequest(
2✔
1584
                        ctx, deviceDeployment.Id, request); err != nil {
2✔
1585
                        return err
×
1586
                }
×
1587
        }
1588
        return nil
2✔
1589
}
1590

1591
// updateDeviceDeploymentStatus will update the deployment status for device of
1592
// ID `deviceID`. Returns nil if update was successful.
1593
func (d *Deployments) UpdateDeviceDeploymentStatus(
1594
        ctx context.Context,
1595
        deviceID, deploymentID string,
1596
        ddState model.DeviceDeploymentState,
1597
) error {
3✔
1598
        deviceDeployment, err := d.db.GetDeviceDeployment(
3✔
1599
                ctx, deviceID, deploymentID, false,
3✔
1600
        )
3✔
1601
        if err == mongo.ErrStorageNotFound {
4✔
1602
                return ErrStorageNotFound
1✔
1603
        } else if err != nil {
3✔
NEW
1604
                return err
×
NEW
1605
        }
×
1606

1607
        return d.updateDeviceDeploymentStatus(ctx, deviceDeployment, ddState)
2✔
1608
}
1609

1610
func (d *Deployments) updateDeviceDeploymentStatus(
1611
        ctx context.Context,
1612
        dd *model.DeviceDeployment,
1613
        ddState model.DeviceDeploymentState,
1614
) error {
5✔
1615

5✔
1616
        l := log.FromContext(ctx)
5✔
1617

5✔
1618
        l.Infof("New status: %s for device %s deployment: %v",
5✔
1619
                ddState.Status, dd.DeviceId, dd.DeploymentId,
5✔
1620
        )
5✔
1621

5✔
1622
        var finishTime *time.Time = nil
5✔
1623
        if model.IsDeviceDeploymentStatusFinished(ddState.Status) {
9✔
1624
                now := time.Now()
4✔
1625
                finishTime = &now
4✔
1626
        }
4✔
1627

1628
        currentStatus := dd.Status
5✔
1629

5✔
1630
        if currentStatus == model.DeviceDeploymentStatusAborted {
5✔
1631
                return ErrDeploymentAborted
×
1632
        }
×
1633

1634
        if currentStatus == model.DeviceDeploymentStatusDecommissioned {
5✔
1635
                return ErrDeviceDecommissioned
×
1636
        }
×
1637

1638
        // nothing to do
1639
        if ddState.Status == currentStatus {
5✔
1640
                return nil
×
1641
        }
×
1642

1643
        // update finish time
1644
        ddState.FinishTime = finishTime
5✔
1645

5✔
1646
        old, err := d.db.UpdateDeviceDeploymentStatus(ctx,
5✔
1647
                dd.DeviceId, dd.DeploymentId, ddState, dd.Status)
5✔
1648
        if err != nil {
5✔
1649
                return err
×
1650
        }
×
1651

1652
        if old != ddState.Status {
10✔
1653
                // fetch deployment stats and update deployment status
5✔
1654
                deployment, err := d.db.FindDeploymentByID(ctx, dd.DeploymentId)
5✔
1655
                if err != nil {
5✔
NEW
1656
                        return errors.Wrap(err, "failed when searching for deployment")
×
NEW
1657
                }
×
1658
                beforeStatus := deployment.GetStatus()
5✔
1659

5✔
1660
                deployment.Stats, err = d.db.UpdateStatsInc(ctx, dd.DeploymentId, old, ddState.Status)
5✔
1661
                if err != nil {
5✔
NEW
1662
                        return err
×
NEW
1663
                }
×
1664
                newStatus := deployment.GetStatus()
5✔
1665
                if beforeStatus != newStatus {
7✔
1666
                        err = d.db.SetDeploymentStatus(ctx, dd.DeploymentId, newStatus, time.Now())
2✔
1667
                        if err != nil {
2✔
NEW
1668
                                return errors.Wrap(err, "failed to update deployment status")
×
NEW
1669
                        }
×
1670
                }
1671
        }
1672

1673
        if !ddState.Status.Active() {
9✔
1674
                l := log.FromContext(ctx)
4✔
1675
                ldd := model.DeviceDeployment{
4✔
1676
                        DeviceId:     dd.DeviceId,
4✔
1677
                        DeploymentId: dd.DeploymentId,
4✔
1678
                        Id:           dd.Id,
4✔
1679
                        Status:       ddState.Status,
4✔
1680
                }
4✔
1681
                if err := d.db.SaveLastDeviceDeploymentStatus(ctx, ldd); err != nil {
4✔
1682
                        l.Error(errors.Wrap(err, "failed to save last device deployment status").Error())
×
1683
                }
×
1684
                if err := d.reindexDevice(ctx, dd.DeviceId); err != nil {
4✔
1685
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1686
                }
×
1687
                if err := d.reindexDeployment(ctx, dd.DeviceId, dd.DeploymentId, dd.Id); err != nil {
4✔
1688
                        l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1689
                }
×
1690
        }
1691

1692
        return nil
5✔
1693
}
1694

1695
func (d *Deployments) GetDeploymentStats(ctx context.Context,
UNCOV
1696
        deploymentID string) (model.Stats, error) {
1✔
UNCOV
1697

1✔
UNCOV
1698
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
UNCOV
1699

1✔
UNCOV
1700
        if err != nil {
1✔
1701
                return nil, errors.Wrap(err, "checking deployment id")
×
1702
        }
×
1703

UNCOV
1704
        if deployment == nil {
1✔
1705
                return nil, nil
×
1706
        }
×
1707

UNCOV
1708
        return deployment.Stats, nil
1✔
1709
}
1710
func (d *Deployments) GetDeploymentsStats(ctx context.Context,
1711
        deploymentIDs ...string) (deploymentStats []*model.DeploymentStats, err error) {
×
1712

×
1713
        deploymentStats, err = d.db.FindDeploymentStatsByIDs(ctx, deploymentIDs...)
×
1714

×
1715
        if err != nil {
×
1716
                return nil, errors.Wrap(err, "checking deployment statistics for IDs")
×
1717
        }
×
1718

1719
        if deploymentStats == nil {
×
1720
                return nil, ErrModelDeploymentNotFound
×
1721
        }
×
1722

1723
        return deploymentStats, nil
×
1724
}
1725

1726
// GetDeviceStatusesForDeployment retrieve device deployment statuses for a given deployment.
1727
func (d *Deployments) GetDeviceStatusesForDeployment(ctx context.Context,
UNCOV
1728
        deploymentID string) ([]model.DeviceDeployment, error) {
1✔
UNCOV
1729

1✔
UNCOV
1730
        deployment, err := d.db.FindDeploymentByID(ctx, deploymentID)
1✔
UNCOV
1731
        if err != nil {
1✔
1732
                return nil, ErrModelInternal
×
1733
        }
×
1734

UNCOV
1735
        if deployment == nil {
1✔
1736
                return nil, ErrModelDeploymentNotFound
×
1737
        }
×
1738

UNCOV
1739
        statuses, err := d.db.GetDeviceStatusesForDeployment(ctx, deploymentID)
1✔
UNCOV
1740
        if err != nil {
1✔
1741
                return nil, ErrModelInternal
×
1742
        }
×
1743

UNCOV
1744
        return statuses, nil
1✔
1745
}
1746

1747
func (d *Deployments) GetDevicesListForDeployment(ctx context.Context,
UNCOV
1748
        query store.ListQuery) ([]model.DeviceDeployment, int, error) {
1✔
UNCOV
1749

1✔
UNCOV
1750
        deployment, err := d.db.FindDeploymentByID(ctx, query.DeploymentID)
1✔
UNCOV
1751
        if err != nil {
1✔
1752
                return nil, -1, ErrModelInternal
×
1753
        }
×
1754

UNCOV
1755
        if deployment == nil {
1✔
1756
                return nil, -1, ErrModelDeploymentNotFound
×
1757
        }
×
1758

UNCOV
1759
        statuses, totalCount, err := d.db.GetDevicesListForDeployment(ctx, query)
1✔
UNCOV
1760
        if err != nil {
1✔
1761
                return nil, -1, ErrModelInternal
×
1762
        }
×
1763

UNCOV
1764
        return statuses, totalCount, nil
1✔
1765
}
1766

1767
func (d *Deployments) GetDeviceDeploymentListForDevice(ctx context.Context,
1768
        query store.ListQueryDeviceDeployments) ([]model.DeviceDeploymentListItem, int, error) {
4✔
1769
        deviceDeployments, totalCount, err := d.db.GetDeviceDeploymentsForDevice(ctx, query)
4✔
1770
        if err != nil {
5✔
1771
                return nil, -1, errors.Wrap(err, "retrieving the list of deployment statuses")
1✔
1772
        }
1✔
1773

1774
        deploymentIDs := make([]string, len(deviceDeployments))
3✔
1775
        for i, deviceDeployment := range deviceDeployments {
9✔
1776
                deploymentIDs[i] = deviceDeployment.DeploymentId
6✔
1777
        }
6✔
1778
        var deployments []*model.Deployment
3✔
1779
        if len(deviceDeployments) > 0 {
6✔
1780
                deployments, _, err = d.db.Find(ctx, model.Query{
3✔
1781
                        IDs:          deploymentIDs,
3✔
1782
                        Limit:        len(deviceDeployments),
3✔
1783
                        DisableCount: true,
3✔
1784
                })
3✔
1785
                if err != nil {
4✔
1786
                        return nil, -1, errors.Wrap(err, "retrieving the list of deployments")
1✔
1787
                }
1✔
1788
        }
1789

1790
        deploymentsMap := make(map[string]*model.Deployment, len(deployments))
2✔
1791
        for _, deployment := range deployments {
5✔
1792
                deploymentsMap[deployment.Id] = deployment
3✔
1793
        }
3✔
1794

1795
        res := make([]model.DeviceDeploymentListItem, 0, len(deviceDeployments))
2✔
1796
        for i, deviceDeployment := range deviceDeployments {
6✔
1797
                if deployment, ok := deploymentsMap[deviceDeployment.DeploymentId]; ok {
7✔
1798
                        res = append(res, model.DeviceDeploymentListItem{
3✔
1799
                                Id:         deviceDeployment.Id,
3✔
1800
                                Deployment: deployment,
3✔
1801
                                Device:     &deviceDeployments[i],
3✔
1802
                        })
3✔
1803
                } else {
4✔
1804
                        res = append(res, model.DeviceDeploymentListItem{
1✔
1805
                                Id:     deviceDeployment.Id,
1✔
1806
                                Device: &deviceDeployments[i],
1✔
1807
                        })
1✔
1808
                }
1✔
1809
        }
1810

1811
        return res, totalCount, nil
2✔
1812
}
1813

1814
func (d *Deployments) setDeploymentDeviceCountIfUnset(
1815
        ctx context.Context,
1816
        deployment *model.Deployment,
1817
) error {
6✔
1818
        if deployment.DeviceCount == nil {
6✔
1819
                deviceCount, err := d.db.DeviceCountByDeployment(ctx, deployment.Id)
×
1820
                if err != nil {
×
1821
                        return errors.Wrap(err, "counting device deployments")
×
1822
                }
×
1823
                err = d.db.SetDeploymentDeviceCount(ctx, deployment.Id, deviceCount)
×
1824
                if err != nil {
×
1825
                        return errors.Wrap(err, "setting the device count for the deployment")
×
1826
                }
×
1827
                deployment.DeviceCount = &deviceCount
×
1828
        }
1829

1830
        return nil
6✔
1831
}
1832

1833
func (d *Deployments) LookupDeployment(ctx context.Context,
UNCOV
1834
        query model.Query) ([]*model.Deployment, int64, error) {
1✔
UNCOV
1835
        list, totalCount, err := d.db.Find(ctx, query)
1✔
UNCOV
1836

1✔
UNCOV
1837
        if err != nil {
1✔
1838
                return nil, 0, errors.Wrap(err, "searching for deployments")
×
1839
        }
×
1840

UNCOV
1841
        if list == nil {
2✔
UNCOV
1842
                return make([]*model.Deployment, 0), 0, nil
1✔
UNCOV
1843
        }
1✔
1844

1845
        for _, deployment := range list {
×
1846
                if err := d.setDeploymentDeviceCountIfUnset(ctx, deployment); err != nil {
×
1847
                        return nil, 0, err
×
1848
                }
×
1849
        }
1850

1851
        return list, totalCount, nil
×
1852
}
1853

1854
// SaveDeviceDeploymentLog will save the deployment log for device of
1855
// ID `deviceID`. Returns nil if log was saved successfully.
1856
func (d *Deployments) SaveDeviceDeploymentLog(ctx context.Context, deviceID string,
UNCOV
1857
        deploymentID string, logs []model.LogMessage) error {
1✔
UNCOV
1858

1✔
UNCOV
1859
        // repack to temporary deployment log and validate
1✔
UNCOV
1860
        dlog := model.DeploymentLog{
1✔
UNCOV
1861
                DeviceID:     deviceID,
1✔
UNCOV
1862
                DeploymentID: deploymentID,
1✔
UNCOV
1863
                Messages:     logs,
1✔
UNCOV
1864
        }
1✔
UNCOV
1865
        if err := dlog.Validate(); err != nil {
1✔
1866
                return errors.Wrapf(err, ErrStorageInvalidLog.Error())
×
1867
        }
×
1868

UNCOV
1869
        if has, err := d.HasDeploymentForDevice(ctx, deploymentID, deviceID); !has {
1✔
1870
                if err != nil {
×
1871
                        return err
×
1872
                } else {
×
1873
                        return ErrModelDeploymentNotFound
×
1874
                }
×
1875
        }
1876

UNCOV
1877
        if err := d.db.SaveDeviceDeploymentLog(ctx, dlog); err != nil {
1✔
1878
                return err
×
1879
        }
×
1880

UNCOV
1881
        return d.db.UpdateDeviceDeploymentLogAvailability(ctx,
1✔
UNCOV
1882
                deviceID, deploymentID, true)
1✔
1883
}
1884

1885
func (d *Deployments) GetDeviceDeploymentLog(ctx context.Context,
UNCOV
1886
        deviceID, deploymentID string) (*model.DeploymentLog, error) {
1✔
UNCOV
1887

1✔
UNCOV
1888
        return d.db.GetDeviceDeploymentLog(ctx,
1✔
UNCOV
1889
                deviceID, deploymentID)
1✔
UNCOV
1890
}
1✔
1891

1892
func (d *Deployments) HasDeploymentForDevice(ctx context.Context,
UNCOV
1893
        deploymentID string, deviceID string) (bool, error) {
1✔
UNCOV
1894
        return d.db.HasDeploymentForDevice(ctx, deploymentID, deviceID)
1✔
UNCOV
1895
}
1✔
1896

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

5✔
1900
        if err := d.db.AbortDeviceDeployments(ctx, deploymentID); err != nil {
6✔
1901
                return err
1✔
1902
        }
1✔
1903

1904
        stats, err := d.db.AggregateDeviceDeploymentByStatus(
4✔
1905
                ctx, deploymentID)
4✔
1906
        if err != nil {
5✔
1907
                return err
1✔
1908
        }
1✔
1909

1910
        // update statistics
1911
        if err := d.db.UpdateStats(ctx, deploymentID, stats); err != nil {
4✔
1912
                return errors.Wrap(err, "failed to update deployment stats")
1✔
1913
        }
1✔
1914

1915
        // when aborting the deployment we need to set status directly instead of
1916
        // using recalcDeploymentStatus method;
1917
        // it is possible that the deployment does not have any device deployments yet;
1918
        // in that case, all statistics are 0 and calculating status based on statistics
1919
        // will not work - the calculated status will be "pending"
1920
        if err := d.db.SetDeploymentStatus(ctx,
2✔
1921
                deploymentID, model.DeploymentStatusFinished, time.Now()); err != nil {
2✔
1922
                return errors.Wrap(err, "failed to update deployment status")
×
1923
        }
×
1924

1925
        return nil
2✔
1926
}
1927

1928
func (d *Deployments) updateDeviceDeploymentsStatus(
1929
        ctx context.Context,
1930
        deviceId string,
1931
        status model.DeviceDeploymentStatus,
1932
) error {
15✔
1933
        var latestDeployment *time.Time
15✔
1934
        // Retrieve active device deployment for the device
15✔
1935
        deviceDeployment, err := d.db.FindOldestActiveDeviceDeployment(ctx, deviceId)
15✔
1936
        if err != nil {
17✔
1937
                return errors.Wrap(err, "Searching for active deployment for the device")
2✔
1938
        } else if deviceDeployment != nil {
17✔
1939
                now := time.Now()
2✔
1940
                ddStatus := model.DeviceDeploymentState{
2✔
1941
                        Status:     status,
2✔
1942
                        FinishTime: &now,
2✔
1943
                }
2✔
1944
                if err := d.updateDeviceDeploymentStatus(
2✔
1945
                        ctx, deviceDeployment, ddStatus,
2✔
1946
                ); err != nil {
2✔
1947
                        return errors.Wrap(err, "updating device deployment status")
×
1948
                }
×
1949
                latestDeployment = deviceDeployment.Created
2✔
1950
        } else {
11✔
1951
                // get latest device deployment for the device
11✔
1952
                deviceDeployment, err := d.db.FindLatestInactiveDeviceDeployment(ctx, deviceId)
11✔
1953
                if err != nil {
11✔
1954
                        return errors.Wrap(err, "Searching for latest active deployment for the device")
×
1955
                } else if deviceDeployment == nil {
20✔
1956
                        latestDeployment = &time.Time{}
9✔
1957
                } else {
11✔
1958
                        latestDeployment = deviceDeployment.Created
2✔
1959
                }
2✔
1960
        }
1961

1962
        // get deployments newer then last device deployment
1963
        // iterate over deployments and check if the device is part of the deployment or not
1964
        // if the device is part of the deployment create new, decommisioned device deployment
1965
        var deploy *model.Deployment
13✔
1966
        deploy, err = d.db.FindNewerActiveDeployment(ctx, latestDeployment, deviceId)
13✔
1967
        if err != nil {
13✔
1968
                return errors.Wrap(err, "Failed to search for newer active deployments")
×
1969
        }
×
1970
        if deploy != nil {
18✔
1971
                deviceDeployment, err = d.createDeviceDeploymentWithStatus(ctx,
5✔
1972
                        deviceId, deploy, status)
5✔
1973
                if err != nil {
5✔
1974
                        return err
×
1975
                }
×
1976
                if !status.Active() {
10✔
1977
                        if err := d.reindexDeployment(ctx, deviceDeployment.DeviceId,
5✔
1978
                                deviceDeployment.DeploymentId, deviceDeployment.Id); err != nil {
5✔
1979
                                l := log.FromContext(ctx)
×
1980
                                l.Warn(errors.Wrap(err, "failed to trigger a deployment reindex"))
×
1981
                        }
×
1982
                }
1983
        }
1984

1985
        if err := d.reindexDevice(ctx, deviceId); err != nil {
13✔
1986
                l := log.FromContext(ctx)
×
1987
                l.Warn(errors.Wrap(err, "failed to trigger a device reindex"))
×
1988
        }
×
1989

1990
        return nil
13✔
1991
}
1992

1993
// DecommissionDevice updates the status of all the pending and active deployments for a device
1994
// to decommissioned
1995
func (d *Deployments) DecommissionDevice(ctx context.Context, deviceId string) error {
7✔
1996
        return d.updateDeviceDeploymentsStatus(
7✔
1997
                ctx,
7✔
1998
                deviceId,
7✔
1999
                model.DeviceDeploymentStatusDecommissioned,
7✔
2000
        )
7✔
2001
}
7✔
2002

2003
// AbortDeviceDeployments aborts all the pending and active deployments for a device
2004
func (d *Deployments) AbortDeviceDeployments(ctx context.Context, deviceId string) error {
8✔
2005
        return d.updateDeviceDeploymentsStatus(
8✔
2006
                ctx,
8✔
2007
                deviceId,
8✔
2008
                model.DeviceDeploymentStatusAborted,
8✔
2009
        )
8✔
2010
}
8✔
2011

2012
// DeleteDeviceDeploymentsHistory deletes the device deployments history
2013
func (d *Deployments) DeleteDeviceDeploymentsHistory(ctx context.Context, deviceId string) error {
2✔
2014
        // get device deployments which will be marked as deleted
2✔
2015
        f := false
2✔
2016
        dd, err := d.db.GetDeviceDeployments(ctx, 0, 0, deviceId, &f, false)
2✔
2017
        if err != nil {
2✔
2018
                return err
×
2019
        }
×
2020

2021
        // no device deployments to update
2022
        if len(dd) <= 0 {
2✔
2023
                return nil
×
2024
        }
×
2025

2026
        // mark device deployments as deleted
2027
        if err := d.db.DeleteDeviceDeploymentsHistory(ctx, deviceId); err != nil {
3✔
2028
                return err
1✔
2029
        }
1✔
2030

2031
        // trigger reindexing of updated device deployments
2032
        deviceDeployments := make([]workflows.DeviceDeploymentShortInfo, len(dd))
1✔
2033
        for i, d := range dd {
2✔
2034
                deviceDeployments[i].ID = d.Id
1✔
2035
                deviceDeployments[i].DeviceID = d.DeviceId
1✔
2036
                deviceDeployments[i].DeploymentID = d.DeploymentId
1✔
2037
        }
1✔
2038
        if d.reportingClient != nil {
2✔
2039
                err = d.workflowsClient.StartReindexReportingDeploymentBatch(ctx, deviceDeployments)
1✔
2040
        }
1✔
2041
        return err
1✔
2042
}
2043

2044
// Storage settings
2045
func (d *Deployments) GetStorageSettings(ctx context.Context) (*model.StorageSettings, error) {
3✔
2046
        settings, err := d.db.GetStorageSettings(ctx)
3✔
2047
        if err != nil {
4✔
2048
                return nil, errors.Wrap(err, "Searching for settings failed")
1✔
2049
        }
1✔
2050

2051
        return settings, nil
2✔
2052
}
2053

2054
func (d *Deployments) SetStorageSettings(
2055
        ctx context.Context,
2056
        storageSettings *model.StorageSettings,
2057
) error {
4✔
2058
        if storageSettings != nil {
8✔
2059
                ctx = storage.SettingsWithContext(ctx, storageSettings)
4✔
2060
                if err := d.objectStorage.HealthCheck(ctx); err != nil {
4✔
2061
                        return errors.WithMessage(err,
×
2062
                                "the provided storage settings failed the health check",
×
2063
                        )
×
2064
                }
×
2065
        }
2066
        if err := d.db.SetStorageSettings(ctx, storageSettings); err != nil {
6✔
2067
                return errors.Wrap(err, "Failed to save settings")
2✔
2068
        }
2✔
2069

2070
        return nil
2✔
2071
}
2072

2073
func (d *Deployments) WithReporting(c reporting.Client) *Deployments {
7✔
2074
        d.reportingClient = c
7✔
2075
        return d
7✔
2076
}
7✔
2077

2078
func (d *Deployments) haveReporting() bool {
6✔
2079
        return d.reportingClient != nil
6✔
2080
}
6✔
2081

2082
func (d *Deployments) search(
2083
        ctx context.Context,
2084
        tid string,
2085
        parms model.SearchParams,
2086
) ([]model.InvDevice, int, error) {
6✔
2087
        if d.haveReporting() {
7✔
2088
                return d.reportingClient.Search(ctx, tid, parms)
1✔
2089
        } else {
6✔
2090
                return d.inventoryClient.Search(ctx, tid, parms)
5✔
2091
        }
5✔
2092
}
2093

2094
func (d *Deployments) UpdateDeploymentsWithArtifactName(
2095
        ctx context.Context,
2096
        artifactName string,
2097
) error {
2✔
2098
        // first check if there are pending deployments with given artifact name
2✔
2099
        exists, err := d.db.ExistUnfinishedByArtifactName(ctx, artifactName)
2✔
2100
        if err != nil {
2✔
2101
                return errors.Wrap(err, "looking for deployments with given artifact name")
×
2102
        }
×
2103
        if !exists {
3✔
UNCOV
2104
                return nil
1✔
UNCOV
2105
        }
1✔
2106

2107
        // Assign artifacts to the deployments with given artifact name
2108
        artifacts, err := d.db.ImagesByName(ctx, artifactName)
1✔
2109
        if err != nil {
1✔
2110
                return errors.Wrap(err, "Finding artifact with given name")
×
2111
        }
×
2112

2113
        if len(artifacts) == 0 {
1✔
2114
                return ErrNoArtifact
×
2115
        }
×
2116
        artifactIDs := getArtifactIDs(artifacts)
1✔
2117
        return d.db.UpdateDeploymentsWithArtifactName(ctx, artifactName, artifactIDs)
1✔
2118
}
2119

2120
func (d *Deployments) reindexDevice(ctx context.Context, deviceID string) error {
25✔
2121
        if d.reportingClient != nil {
27✔
2122
                return d.workflowsClient.StartReindexReporting(ctx, deviceID)
2✔
2123
        }
2✔
2124
        return nil
23✔
2125
}
2126

2127
func (d *Deployments) reindexDeployment(ctx context.Context,
2128
        deviceID, deploymentID, ID string) error {
17✔
2129
        if d.reportingClient != nil {
19✔
2130
                return d.workflowsClient.StartReindexReportingDeployment(ctx, deviceID, deploymentID, ID)
2✔
2131
        }
2✔
2132
        return nil
15✔
2133
}
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