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

mendersoftware / deployments / 1022633108

02 Oct 2023 08:56AM UTC coverage: 80.458% (-0.03%) from 80.483%
1022633108

Pull #931

gitlab-ci

merlin-northern
feat: direct upload with skip verify: set metadata on complete

Changelog: Title
Ticket: MEN-6696
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #931: feat: direct upload with skip verify: set metadata on complete

21 of 76 new or added lines in 4 files covered. (27.63%)

84 existing lines in 1 file now uncovered.

7835 of 9738 relevant lines covered (80.46%)

34.99 hits per line

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

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

15
package http
16

17
import (
18
        "context"
19
        "encoding/json"
20
        "fmt"
21
        "io"
22
        "mime/multipart"
23
        "net/http"
24
        "net/url"
25
        "strconv"
26
        "strings"
27
        "time"
28

29
        "github.com/ant0ine/go-json-rest/rest"
30
        "github.com/asaskevich/govalidator"
31
        "github.com/pkg/errors"
32

33
        "github.com/mendersoftware/go-lib-micro/config"
34
        "github.com/mendersoftware/go-lib-micro/identity"
35
        "github.com/mendersoftware/go-lib-micro/log"
36
        "github.com/mendersoftware/go-lib-micro/requestid"
37
        "github.com/mendersoftware/go-lib-micro/requestlog"
38
        "github.com/mendersoftware/go-lib-micro/rest_utils"
39

40
        "github.com/mendersoftware/deployments/app"
41
        dconfig "github.com/mendersoftware/deployments/config"
42
        "github.com/mendersoftware/deployments/model"
43
        "github.com/mendersoftware/deployments/store"
44
        "github.com/mendersoftware/deployments/utils"
45
)
46

47
func init() {
2✔
48
        rest.ErrorFieldName = "error"
2✔
49
}
2✔
50

51
const (
52
        // 15 minutes
53
        DefaultDownloadLinkExpire = 15 * time.Minute
54
        // 10 Mb
55
        DefaultMaxMetaSize  = 1024 * 1024 * 10
56
        DefaultMaxImageSize = 10 * 1024 * 1024 * 1024 // 10GiB
57

58
        // Pagination
59
        DefaultPerPage                      = 20
60
        MaximumPerPage                      = 500
61
        MaximumPerPageListDeviceDeployments = 20
62
)
63

64
const (
65
        // Header Constants
66
        hdrTotalCount    = "X-Total-Count"
67
        hdrForwardedHost = "X-Forwarded-Host"
68
)
69

70
// storage keys
71
const (
72
        // Common HTTP form parameters
73
        ParamArtifactName = "artifact_name"
74
        ParamDeviceType   = "device_type"
75
        ParamUpdateType   = "update_type"
76
        ParamDeploymentID = "deployment_id"
77
        ParamDeviceID     = "device_id"
78
        ParamTenantID     = "tenant_id"
79
        ParamName         = "name"
80
        ParamTag          = "tag"
81
        ParamDescription  = "description"
82
        ParamPage         = "page"
83
        ParamPerPage      = "per_page"
84
        ParamSort         = "sort"
85
        ParamID           = "id"
86
)
87

88
const Redacted = "REDACTED"
89

90
// JWT token
91
const (
92
        HTTPHeaderAuthorization       = "Authorization"
93
        HTTPHeaderAuthorizationBearer = "Bearer"
94
)
95

96
const (
97
        defaultTimeout = time.Second * 10
98
)
99

100
// Errors
101
var (
102
        ErrIDNotUUID                      = errors.New("ID is not a valid UUID")
103
        ErrEmptyID                        = errors.New("id: cannot be blank")
104
        ErrArtifactUsedInActiveDeployment = errors.New("Artifact is used in active deployment")
105
        ErrInvalidExpireParam             = errors.New("Invalid expire parameter")
106
        ErrArtifactNameMissing            = errors.New(
107
                "request does not contain the name of the artifact",
108
        )
109
        ErrArtifactTypeMissing = errors.New(
110
                "request does not contain the type of artifact",
111
        )
112
        ErrArtifactDeviceTypesCompatibleMissing = errors.New(
113
                "request does not contain the list of compatible device types",
114
        )
115
        ErrArtifactFileMissing       = errors.New("request does not contain the artifact file")
116
        ErrModelArtifactFileTooLarge = errors.New("Artifact file too large")
117

118
        ErrInternal                   = errors.New("Internal error")
119
        ErrDeploymentAlreadyFinished  = errors.New("Deployment already finished")
120
        ErrUnexpectedDeploymentStatus = errors.New("Unexpected deployment status")
121
        ErrMissingIdentity            = errors.New("Missing identity data")
122
        ErrMissingSize                = errors.New("missing size form-data")
123
        ErrMissingGroupName           = errors.New("Missing group name")
124

125
        ErrInvalidSortDirection = fmt.Errorf("invalid form value: must be one of \"%s\" or \"%s\"",
126
                model.SortDirectionAscending, model.SortDirectionDescending)
127
)
128

129
type Config struct {
130
        // URL signing parameters:
131

132
        // PresignSecret holds the secret value used by the signature algorithm.
133
        PresignSecret []byte
134
        // PresignExpire duration until the link expires.
135
        PresignExpire time.Duration
136
        // PresignHostname is the signed url hostname.
137
        PresignHostname string
138
        // PresignScheme is the URL scheme used for generating signed URLs.
139
        PresignScheme string
140
        // MaxImageSize is the maximum image size
141
        MaxImageSize int64
142

143
        EnableDirectUpload bool
144
        // EnableDirectUploadSkipVerify allows turning off the verification of uploaded artifacts
145
        EnableDirectUploadSkipVerify bool
146
}
147

148
func NewConfig() *Config {
205✔
149
        return &Config{
205✔
150
                PresignExpire: DefaultDownloadLinkExpire,
205✔
151
                PresignScheme: "https",
205✔
152
                MaxImageSize:  DefaultMaxImageSize,
205✔
153
        }
205✔
154
}
205✔
155

156
func (conf *Config) SetPresignSecret(key []byte) *Config {
20✔
157
        conf.PresignSecret = key
20✔
158
        return conf
20✔
159
}
20✔
160

161
func (conf *Config) SetPresignExpire(duration time.Duration) *Config {
14✔
162
        conf.PresignExpire = duration
14✔
163
        return conf
14✔
164
}
14✔
165

166
func (conf *Config) SetPresignHostname(hostname string) *Config {
12✔
167
        conf.PresignHostname = hostname
12✔
168
        return conf
12✔
169
}
12✔
170

171
func (conf *Config) SetPresignScheme(scheme string) *Config {
14✔
172
        conf.PresignScheme = scheme
14✔
173
        return conf
14✔
174
}
14✔
175

176
func (conf *Config) SetMaxImageSize(size int64) *Config {
1✔
177
        conf.MaxImageSize = size
1✔
178
        return conf
1✔
179
}
1✔
180

181
func (conf *Config) SetEnableDirectUpload(enable bool) *Config {
12✔
182
        conf.EnableDirectUpload = enable
12✔
183
        return conf
12✔
184
}
12✔
185

186
func (conf *Config) SetEnableDirectUploadSkipVerify(enable bool) *Config {
1✔
187
        conf.EnableDirectUploadSkipVerify = enable
1✔
188
        return conf
1✔
189
}
1✔
190

191
type DeploymentsApiHandlers struct {
192
        view   RESTView
193
        store  store.DataStore
194
        app    app.App
195
        config Config
196
}
197

198
func NewDeploymentsApiHandlers(
199
        store store.DataStore,
200
        view RESTView,
201
        app app.App,
202
        config ...*Config,
203
) *DeploymentsApiHandlers {
175✔
204
        conf := NewConfig()
175✔
205
        for _, c := range config {
207✔
206
                if c == nil {
33✔
207
                        continue
1✔
208
                }
209
                if c.PresignSecret != nil {
51✔
210
                        conf.PresignSecret = c.PresignSecret
20✔
211
                }
20✔
212
                if c.PresignExpire != 0 {
62✔
213
                        conf.PresignExpire = c.PresignExpire
31✔
214
                }
31✔
215
                if c.PresignHostname != "" {
43✔
216
                        conf.PresignHostname = c.PresignHostname
12✔
217
                }
12✔
218
                if c.PresignScheme != "" {
62✔
219
                        conf.PresignScheme = c.PresignScheme
31✔
220
                }
31✔
221
                if c.MaxImageSize > 0 {
62✔
222
                        conf.MaxImageSize = c.MaxImageSize
31✔
223
                }
31✔
224
                conf.EnableDirectUpload = c.EnableDirectUpload
31✔
225
                conf.EnableDirectUploadSkipVerify = c.EnableDirectUploadSkipVerify
31✔
226
        }
227
        return &DeploymentsApiHandlers{
175✔
228
                store:  store,
175✔
229
                view:   view,
175✔
230
                app:    app,
175✔
231
                config: *conf,
175✔
232
        }
175✔
233
}
234

235
func (d *DeploymentsApiHandlers) AliveHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
236
        w.WriteHeader(http.StatusNoContent)
1✔
237
}
1✔
238

239
func (d *DeploymentsApiHandlers) HealthHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
240
        ctx := r.Context()
2✔
241
        l := log.FromContext(ctx)
2✔
242
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
2✔
243
        defer cancel()
2✔
244

2✔
245
        err := d.app.HealthCheck(ctx)
2✔
246
        if err != nil {
3✔
247
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
248
                return
1✔
249
        }
1✔
250
        w.WriteHeader(http.StatusNoContent)
1✔
251
}
252

253
func getReleaseOrImageFilter(r *rest.Request, version listReleasesVersion,
254
        paginated bool) *model.ReleaseOrImageFilter {
31✔
255

31✔
256
        q := r.URL.Query()
31✔
257

31✔
258
        filter := &model.ReleaseOrImageFilter{
31✔
259
                Name:       q.Get(ParamName),
31✔
260
                UpdateType: q.Get(ParamUpdateType),
31✔
261
        }
31✔
262
        if version == listReleasesV1 {
56✔
263
                filter.Description = q.Get(ParamDescription)
25✔
264
                filter.DeviceType = q.Get(ParamDeviceType)
25✔
265
        } else if version == listReleasesV2 {
39✔
266
                filter.Tags = q[ParamTag]
7✔
267
        }
7✔
268

269
        if paginated {
48✔
270
                filter.Sort = q.Get(ParamSort)
17✔
271
                if page := q.Get(ParamPage); page != "" {
18✔
272
                        if i, err := strconv.Atoi(page); err == nil {
2✔
273
                                filter.Page = i
1✔
274
                        }
1✔
275
                }
276
                if perPage := q.Get(ParamPerPage); perPage != "" {
19✔
277
                        if i, err := strconv.Atoi(perPage); err == nil {
4✔
278
                                filter.PerPage = i
2✔
279
                        }
2✔
280
                }
281
                if filter.Page <= 0 {
33✔
282
                        filter.Page = 1
16✔
283
                }
16✔
284
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
33✔
285
                        filter.PerPage = DefaultPerPage
16✔
286
                }
16✔
287
        }
288

289
        return filter
31✔
290
}
291

292
type limitResponse struct {
293
        Limit uint64 `json:"limit"`
294
        Usage uint64 `json:"usage"`
295
}
296

297
func (d *DeploymentsApiHandlers) GetLimit(w rest.ResponseWriter, r *rest.Request) {
3✔
298
        l := requestlog.GetRequestLogger(r)
3✔
299

3✔
300
        name := r.PathParam("name")
3✔
301

3✔
302
        if !model.IsValidLimit(name) {
4✔
303
                d.view.RenderError(w, r,
1✔
304
                        errors.Errorf("unsupported limit %s", name),
1✔
305
                        http.StatusBadRequest, l)
1✔
306
                return
1✔
307
        }
1✔
308

309
        limit, err := d.app.GetLimit(r.Context(), name)
2✔
310
        if err != nil {
3✔
311
                d.view.RenderInternalError(w, r, err, l)
1✔
312
                return
1✔
313
        }
1✔
314

315
        d.view.RenderSuccessGet(w, limitResponse{
1✔
316
                Limit: limit.Value,
1✔
317
                Usage: 0, // TODO fill this when ready
1✔
318
        })
1✔
319
}
320

321
// images
322

323
func (d *DeploymentsApiHandlers) GetImage(w rest.ResponseWriter, r *rest.Request) {
1✔
324
        l := requestlog.GetRequestLogger(r)
1✔
325

1✔
326
        id := r.PathParam("id")
1✔
327

1✔
328
        if !govalidator.IsUUID(id) {
2✔
329
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
330
                return
1✔
331
        }
1✔
332

333
        image, err := d.app.GetImage(r.Context(), id)
1✔
334
        if err != nil {
1✔
335
                d.view.RenderInternalError(w, r, err, l)
×
336
                return
×
337
        }
×
338

339
        if image == nil {
2✔
340
                d.view.RenderErrorNotFound(w, r, l)
1✔
341
                return
1✔
342
        }
1✔
343

344
        d.view.RenderSuccessGet(w, image)
1✔
345
}
346

347
func (d *DeploymentsApiHandlers) GetImages(w rest.ResponseWriter, r *rest.Request) {
5✔
348
        l := requestlog.GetRequestLogger(r)
5✔
349

5✔
350
        defer redactReleaseName(r)
5✔
351
        filter := getReleaseOrImageFilter(r, listReleasesV1, false)
5✔
352

5✔
353
        list, _, err := d.app.ListImages(r.Context(), filter)
5✔
354
        if err != nil {
6✔
355
                d.view.RenderInternalError(w, r, err, l)
1✔
356
                return
1✔
357
        }
1✔
358

359
        d.view.RenderSuccessGet(w, list)
4✔
360
}
361

362
func (d *DeploymentsApiHandlers) ListImages(w rest.ResponseWriter, r *rest.Request) {
4✔
363
        l := requestlog.GetRequestLogger(r)
4✔
364

4✔
365
        defer redactReleaseName(r)
4✔
366
        filter := getReleaseOrImageFilter(r, listReleasesV1, true)
4✔
367

4✔
368
        list, totalCount, err := d.app.ListImages(r.Context(), filter)
4✔
369
        if err != nil {
5✔
370
                d.view.RenderInternalError(w, r, err, l)
1✔
371
                return
1✔
372
        }
1✔
373

374
        hasNext := totalCount > int(filter.Page*filter.PerPage)
3✔
375
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
3✔
376
        for _, l := range links {
6✔
377
                w.Header().Add("Link", l)
3✔
378
        }
3✔
379
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
380

3✔
381
        d.view.RenderSuccessGet(w, list)
3✔
382
}
383

384
func (d *DeploymentsApiHandlers) DownloadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
385
        l := requestlog.GetRequestLogger(r)
1✔
386

1✔
387
        id := r.PathParam("id")
1✔
388

1✔
389
        if !govalidator.IsUUID(id) {
1✔
390
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
391
                return
×
392
        }
×
393

394
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
395
        link, err := d.app.DownloadLink(r.Context(), id, time.Duration(expireSeconds)*time.Second)
1✔
396
        if err != nil {
1✔
397
                d.view.RenderInternalError(w, r, err, l)
×
398
                return
×
399
        }
×
400

401
        if link == nil {
1✔
402
                d.view.RenderErrorNotFound(w, r, l)
×
403
                return
×
404
        }
×
405

406
        d.view.RenderSuccessGet(w, link)
1✔
407
}
408

409
func (d *DeploymentsApiHandlers) UploadLink(w rest.ResponseWriter, r *rest.Request) {
4✔
410
        l := requestlog.GetRequestLogger(r)
4✔
411

4✔
412
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
4✔
413
        link, err := d.app.UploadLink(
4✔
414
                r.Context(),
4✔
415
                time.Duration(expireSeconds)*time.Second,
4✔
416
                d.config.EnableDirectUploadSkipVerify,
4✔
417
        )
4✔
418
        if err != nil {
5✔
419
                d.view.RenderInternalError(w, r, err, l)
1✔
420
                return
1✔
421
        }
1✔
422

423
        if link == nil {
4✔
424
                d.view.RenderErrorNotFound(w, r, l)
1✔
425
                return
1✔
426
        }
1✔
427

428
        d.view.RenderSuccessGet(w, link)
2✔
429
}
430

431
const maxMetadataSize = 2048
432

433
func (d *DeploymentsApiHandlers) CompleteUpload(w rest.ResponseWriter, r *rest.Request) {
4✔
434
        ctx := r.Context()
4✔
435
        l := log.FromContext(ctx)
4✔
436

4✔
437
        artifactID := r.PathParam(ParamID)
4✔
438

4✔
439
        var metadata *model.DirectUploadMetadata
4✔
440
        if d.config.EnableDirectUploadSkipVerify {
5✔
441
                var directMetadata model.DirectUploadMetadata
1✔
442
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
443
                n, err := io.ReadFull(r.Body, bodyBuffer)
1✔
444
                r.Body.Close()
1✔
445
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
NEW
446
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
NEW
447
                }
×
448
                err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
449
                if err == nil {
1✔
NEW
450
                        if directMetadata.Validate() == nil {
×
NEW
451
                                metadata = &directMetadata
×
NEW
452
                        }
×
453
                } else {
1✔
454
                        l.Errorf("error parsing json data: %s", err.Error())
1✔
455
                }
1✔
456
        }
457

458
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
4✔
459
        switch errors.Cause(err) {
4✔
460
        case nil:
2✔
461
                // w.Header().Set("Link", "FEAT: Upload status API")
2✔
462
                w.WriteHeader(http.StatusAccepted)
2✔
463
        case app.ErrUploadNotFound:
1✔
464
                d.view.RenderErrorNotFound(w, r, l)
1✔
465
        default:
1✔
466
                l.Error(err)
1✔
467
                w.WriteHeader(http.StatusInternalServerError)
1✔
468
                w.WriteJson(rest_utils.ApiError{ // nolint:errcheck
1✔
469
                        Err:   "internal server error",
1✔
470
                        ReqId: requestid.FromContext(ctx),
1✔
471
                })
1✔
472
        }
473
}
474

475
func (d *DeploymentsApiHandlers) DownloadConfiguration(w rest.ResponseWriter, r *rest.Request) {
10✔
476
        if d.config.PresignSecret == nil {
11✔
477
                rest.NotFound(w, r)
1✔
478
                return
1✔
479
        }
1✔
480
        var (
9✔
481
                deviceID, _     = url.PathUnescape(r.PathParam(ParamDeviceID))
9✔
482
                deviceType, _   = url.PathUnescape(r.PathParam(ParamDeviceType))
9✔
483
                deploymentID, _ = url.PathUnescape(r.PathParam(ParamDeploymentID))
9✔
484
        )
9✔
485
        if deviceID == "" || deviceType == "" || deploymentID == "" {
9✔
UNCOV
486
                rest.NotFound(w, r)
×
487
                return
×
488
        }
×
489

490
        var (
9✔
491
                tenantID string
9✔
492
                l        = log.FromContext(r.Context())
9✔
493
                q        = r.URL.Query()
9✔
494
                err      error
9✔
495
        )
9✔
496
        tenantID = q.Get(ParamTenantID)
9✔
497
        sig := model.NewRequestSignature(r.Request, d.config.PresignSecret)
9✔
498
        if err = sig.Validate(); err != nil {
12✔
499
                switch cause := errors.Cause(err); cause {
3✔
500
                case model.ErrLinkExpired:
1✔
501
                        d.view.RenderError(w, r, cause, http.StatusForbidden, l)
1✔
502
                default:
2✔
503
                        d.view.RenderError(w, r,
2✔
504
                                errors.Wrap(err, "invalid request parameters"),
2✔
505
                                http.StatusBadRequest, l,
2✔
506
                        )
2✔
507
                }
508
                return
3✔
509
        }
510

511
        if !sig.VerifyHMAC256() {
9✔
512
                d.view.RenderError(w, r,
2✔
513
                        errors.New("signature invalid"),
2✔
514
                        http.StatusForbidden, l,
2✔
515
                )
2✔
516
                return
2✔
517
        }
2✔
518

519
        // Validate request signature
520
        ctx := identity.WithContext(r.Context(), &identity.Identity{
6✔
521
                Subject:  deviceID,
6✔
522
                Tenant:   tenantID,
6✔
523
                IsDevice: true,
6✔
524
        })
6✔
525

6✔
526
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
6✔
527
        if err != nil {
8✔
528
                switch cause := errors.Cause(err); cause {
2✔
529
                case app.ErrModelDeploymentNotFound:
1✔
530
                        d.view.RenderError(w, r,
1✔
531
                                errors.Errorf(
1✔
532
                                        "deployment with id '%s' not found",
1✔
533
                                        deploymentID,
1✔
534
                                ),
1✔
535
                                http.StatusNotFound, l,
1✔
536
                        )
1✔
537
                default:
1✔
538
                        l.Error(err.Error())
1✔
539
                        d.view.RenderInternalError(w, r, err, l)
1✔
540
                }
541
                return
2✔
542
        }
543
        artifactPayload, err := io.ReadAll(artifact)
4✔
544
        if err != nil {
5✔
545
                l.Error(err.Error())
1✔
546
                d.view.RenderInternalError(w, r, err, l)
1✔
547
                return
1✔
548
        }
1✔
549

550
        rw := w.(http.ResponseWriter)
3✔
551
        hdr := rw.Header()
3✔
552
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
3✔
553
        hdr.Set("Content-Type", app.ArtifactContentType)
3✔
554
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
3✔
555
        rw.WriteHeader(http.StatusOK)
3✔
556
        _, err = rw.Write(artifactPayload)
3✔
557
        if err != nil {
3✔
UNCOV
558
                // There's not anything we can do here in terms of the response.
×
559
                l.Error(err.Error())
×
560
        }
×
561
}
562

563
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
1✔
564
        l := requestlog.GetRequestLogger(r)
1✔
565

1✔
566
        id := r.PathParam("id")
1✔
567

1✔
568
        if !govalidator.IsUUID(id) {
1✔
UNCOV
569
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
570
                return
×
571
        }
×
572

573
        if err := d.app.DeleteImage(r.Context(), id); err != nil {
2✔
574
                switch err {
1✔
UNCOV
575
                default:
×
576
                        d.view.RenderInternalError(w, r, err, l)
×
577
                case app.ErrImageMetaNotFound:
×
578
                        d.view.RenderErrorNotFound(w, r, l)
×
579
                case app.ErrModelImageInActiveDeployment:
1✔
580
                        d.view.RenderError(w, r, ErrArtifactUsedInActiveDeployment, http.StatusConflict, l)
1✔
581
                }
582
                return
1✔
583
        }
584

585
        d.view.RenderSuccessDelete(w)
1✔
586
}
587

UNCOV
588
func (d *DeploymentsApiHandlers) EditImage(w rest.ResponseWriter, r *rest.Request) {
×
589
        l := requestlog.GetRequestLogger(r)
×
590

×
591
        id := r.PathParam("id")
×
592

×
593
        if !govalidator.IsUUID(id) {
×
594
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
595
                return
×
596
        }
×
597

UNCOV
598
        constructor, err := getImageMetaFromBody(r)
×
599
        if err != nil {
×
600
                d.view.RenderError(
×
601
                        w,
×
602
                        r,
×
603
                        errors.Wrap(err, "Validating request body"),
×
604
                        http.StatusBadRequest,
×
605
                        l,
×
606
                )
×
607
                return
×
608
        }
×
609

UNCOV
610
        found, err := d.app.EditImage(r.Context(), id, constructor)
×
611
        if err != nil {
×
612
                if err == app.ErrModelImageUsedInAnyDeployment {
×
613
                        d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
614
                        return
×
615
                }
×
616
                d.view.RenderInternalError(w, r, err, l)
×
617
                return
×
618
        }
619

UNCOV
620
        if !found {
×
621
                d.view.RenderErrorNotFound(w, r, l)
×
622
                return
×
623
        }
×
624

UNCOV
625
        d.view.RenderSuccessPut(w)
×
626
}
627

UNCOV
628
func getImageMetaFromBody(r *rest.Request) (*model.ImageMeta, error) {
×
629

×
630
        var constructor *model.ImageMeta
×
631

×
632
        if err := r.DecodeJsonPayload(&constructor); err != nil {
×
633
                return nil, err
×
634
        }
×
635

UNCOV
636
        if err := constructor.Validate(); err != nil {
×
637
                return nil, err
×
638
        }
×
639

UNCOV
640
        return constructor, nil
×
641
}
642

643
// NewImage is the Multipart Image/Meta upload handler.
644
// Request should be of type "multipart/form-data". The parts are
645
// key/value pairs of metadata information except the last one,
646
// which must contain the artifact file.
647
func (d *DeploymentsApiHandlers) NewImage(w rest.ResponseWriter, r *rest.Request) {
7✔
648
        d.newImageWithContext(r.Context(), w, r)
7✔
649
}
7✔
650

651
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
652
        l := requestlog.GetRequestLogger(r)
7✔
653

7✔
654
        tenantID := r.PathParam("tenant")
7✔
655

7✔
656
        if tenantID == "" {
7✔
UNCOV
657
                rest_utils.RestErrWithLog(
×
658
                        w,
×
659
                        r,
×
660
                        l,
×
661
                        fmt.Errorf("missing tenant id in path"),
×
662
                        http.StatusBadRequest,
×
663
                )
×
664
                return
×
665
        }
×
666

667
        var ctx context.Context
7✔
668
        if tenantID != "default" {
8✔
669
                ident := &identity.Identity{Tenant: tenantID}
1✔
670
                ctx = identity.WithContext(r.Context(), ident)
1✔
671
        } else {
7✔
672
                ctx = r.Context()
6✔
673
        }
6✔
674

675
        d.newImageWithContext(ctx, w, r)
7✔
676
}
677

678
func (d *DeploymentsApiHandlers) newImageWithContext(
679
        ctx context.Context,
680
        w rest.ResponseWriter,
681
        r *rest.Request,
682
) {
13✔
683
        l := requestlog.GetRequestLogger(r)
13✔
684

13✔
685
        formReader, err := r.MultipartReader()
13✔
686
        if err != nil {
17✔
687
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
688
                return
4✔
689
        }
4✔
690

691
        // parse multipart message
692
        multipartUploadMsg, err := d.ParseMultipart(formReader)
9✔
693

9✔
694
        if err != nil {
14✔
695
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
5✔
696
                return
5✔
697
        }
5✔
698

699
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
5✔
700
        if err == nil {
8✔
701
                d.view.RenderSuccessPost(w, r, imgID)
3✔
702
                return
3✔
703
        }
3✔
704
        var cErr *model.ConflictError
3✔
705
        if errors.As(err, &cErr) {
5✔
706
                w.WriteHeader(http.StatusConflict)
2✔
707
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
2✔
708
                err = w.WriteJson(cErr)
2✔
709
                if err != nil {
2✔
UNCOV
710
                        l.Error(err)
×
711
                } else {
2✔
712
                        l.Error(cErr.Error())
2✔
713
                }
2✔
714
                return
2✔
715
        }
716
        cause := errors.Cause(err)
1✔
717
        switch cause {
1✔
UNCOV
718
        default:
×
719
                d.view.RenderInternalError(w, r, err, l)
×
720
                return
×
721
        case app.ErrModelArtifactNotUnique:
×
722
                l.Error(err.Error())
×
723
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
724
                return
×
725
        case app.ErrModelParsingArtifactFailed:
1✔
726
                l.Error(err.Error())
1✔
727
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
728
                return
1✔
729
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
730
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
UNCOV
731
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
732
                l.Error(err.Error())
×
733
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
734
                return
×
735
        }
736
}
737

738
func formatArtifactUploadError(err error) error {
2✔
739
        // remove generic message
2✔
740
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
741

2✔
742
        // handle specific cases
2✔
743

2✔
744
        if strings.Contains(errMsg, "invalid checksum") {
2✔
UNCOV
745
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
746
        }
×
747

748
        if strings.Contains(errMsg, "unsupported version") {
2✔
UNCOV
749
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
750
                        "; supported versions are: 1, 2")
×
751
        }
×
752

753
        return errors.New(errMsg)
2✔
754
}
755

756
// GenerateImage s the multipart Raw Data/Meta upload handler.
757
// Request should be of type "multipart/form-data". The parts are
758
// key/valyue pairs of metadata information except the last one,
759
// which must contain the file containing the raw data to be processed
760
// into an artifact.
761
func (d *DeploymentsApiHandlers) GenerateImage(w rest.ResponseWriter, r *rest.Request) {
13✔
762
        l := requestlog.GetRequestLogger(r)
13✔
763

13✔
764
        formReader, err := r.MultipartReader()
13✔
765
        if err != nil {
15✔
766
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
767
                return
2✔
768
        }
2✔
769

770
        // parse multipart message
771
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
11✔
772
        if err != nil {
15✔
773
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
774
                return
4✔
775
        }
4✔
776

777
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
7✔
778
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
14✔
779
                multipartMsg.Token = tokenFields[1]
7✔
780
        }
7✔
781

782
        imgID, err := d.app.GenerateImage(r.Context(), multipartMsg)
7✔
783
        cause := errors.Cause(err)
7✔
784
        switch cause {
7✔
785
        default:
1✔
786
                d.view.RenderInternalError(w, r, err, l)
1✔
787
        case nil:
3✔
788
                d.view.RenderSuccessPost(w, r, imgID)
3✔
789
        case app.ErrModelArtifactNotUnique:
1✔
790
                l.Error(err.Error())
1✔
791
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
1✔
792
        case app.ErrModelParsingArtifactFailed:
1✔
793
                l.Error(err.Error())
1✔
794
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
795
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
796
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
797
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
798
                l.Error(err.Error())
1✔
799
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
1✔
800
        }
801
}
802

803
// ParseMultipart parses multipart/form-data message.
804
func (d *DeploymentsApiHandlers) ParseMultipart(
805
        r *multipart.Reader,
806
) (*model.MultipartUploadMsg, error) {
9✔
807
        uploadMsg := &model.MultipartUploadMsg{
9✔
808
                MetaConstructor: &model.ImageMeta{},
9✔
809
        }
9✔
810
        var size int64
9✔
811
        // Parse the multipart form sequentially. To remain backward compatible
9✔
812
        // all form names that are not part of the API are ignored.
9✔
813
        for {
38✔
814
                part, err := r.NextPart()
29✔
815
                if err != nil {
31✔
816
                        if err == io.EOF {
4✔
817
                                // The whole message has been consumed without
2✔
818
                                // the "artifact" form part.
2✔
819
                                return nil, ErrArtifactFileMissing
2✔
820
                        }
2✔
UNCOV
821
                        return nil, err
×
822
                }
823
                switch strings.ToLower(part.FormName()) {
27✔
824
                case "description":
7✔
825
                        // Add description to the metadata
7✔
826
                        dscr, err := io.ReadAll(part)
7✔
827
                        if err != nil {
7✔
UNCOV
828
                                return nil, err
×
829
                        }
×
830
                        uploadMsg.MetaConstructor.Description = string(dscr)
7✔
831

832
                case "size":
7✔
833
                        // Add size limit to the metadata
7✔
834
                        sz, err := io.ReadAll(part)
7✔
835
                        if err != nil {
7✔
UNCOV
836
                                return nil, err
×
837
                        }
×
838
                        size, err = strconv.ParseInt(string(sz), 10, 64)
7✔
839
                        if err != nil {
7✔
UNCOV
840
                                return nil, err
×
841
                        }
×
842
                        // Add one since this will impose the upper limit on the
843
                        // artifact size.
844
                        if size > d.config.MaxImageSize {
7✔
UNCOV
845
                                return nil, ErrModelArtifactFileTooLarge
×
846
                        }
×
847

848
                case "artifact_id":
7✔
849
                        // Add artifact id to the metadata (must be a valid UUID).
7✔
850
                        b, err := io.ReadAll(part)
7✔
851
                        if err != nil {
7✔
UNCOV
852
                                return nil, err
×
853
                        }
×
854
                        id := string(b)
7✔
855
                        if !govalidator.IsUUID(id) {
10✔
856
                                return nil, errors.New(
3✔
857
                                        "artifact_id is not a valid UUID",
3✔
858
                                )
3✔
859
                        }
3✔
860
                        uploadMsg.ArtifactID = id
4✔
861

862
                case "artifact":
5✔
863
                        // Assign the form-data payload to the artifact reader
5✔
864
                        // and return. The content is consumed elsewhere.
5✔
865
                        if size > 0 {
10✔
866
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
5✔
867
                        } else {
5✔
UNCOV
868
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
×
869
                                        part,
×
870
                                        d.config.MaxImageSize,
×
871
                                )
×
872
                        }
×
873
                        return uploadMsg, nil
5✔
874

875
                default:
4✔
876
                        // Ignore all non-API sections.
4✔
877
                        continue
4✔
878
                }
879
        }
880
}
881

882
// ParseGenerateImageMultipart parses multipart/form-data message.
883
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
884
        r *multipart.Reader,
885
) (*model.MultipartGenerateImageMsg, error) {
11✔
886
        msg := &model.MultipartGenerateImageMsg{}
11✔
887
        var size int64
11✔
888

11✔
889
ParseLoop:
11✔
890
        for {
65✔
891
                part, err := r.NextPart()
54✔
892
                if err != nil {
56✔
893
                        if err == io.EOF {
4✔
894
                                break
2✔
895
                        }
UNCOV
896
                        return nil, err
×
897
                }
898
                switch strings.ToLower(part.FormName()) {
52✔
899
                case "args":
7✔
900
                        b, err := io.ReadAll(part)
7✔
901
                        if err != nil {
7✔
UNCOV
902
                                return nil, errors.Wrap(err,
×
903
                                        "failed to read form value 'args'",
×
904
                                )
×
905
                        }
×
906
                        msg.Args = string(b)
7✔
907

908
                case "description":
7✔
909
                        b, err := io.ReadAll(part)
7✔
910
                        if err != nil {
7✔
UNCOV
911
                                return nil, errors.Wrap(err,
×
912
                                        "failed to read form value 'description'",
×
913
                                )
×
914
                        }
×
915
                        msg.Description = string(b)
7✔
916

917
                case "device_types_compatible":
9✔
918
                        b, err := io.ReadAll(part)
9✔
919
                        if err != nil {
9✔
UNCOV
920
                                return nil, errors.Wrap(err,
×
921
                                        "failed to read form value 'device_types_compatible'",
×
922
                                )
×
923
                        }
×
924
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
9✔
925

926
                case "file":
9✔
927
                        if size > 0 {
15✔
928
                                msg.FileReader = utils.ReadExactly(part, size)
6✔
929
                        } else {
9✔
930
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxImageSize)
3✔
931
                        }
3✔
932
                        break ParseLoop
9✔
933

934
                case "name":
10✔
935
                        b, err := io.ReadAll(part)
10✔
936
                        if err != nil {
10✔
UNCOV
937
                                return nil, errors.Wrap(err,
×
938
                                        "failed to read form value 'name'",
×
939
                                )
×
940
                        }
×
941
                        msg.Name = string(b)
10✔
942

943
                case "type":
9✔
944
                        b, err := io.ReadAll(part)
9✔
945
                        if err != nil {
9✔
UNCOV
946
                                return nil, errors.Wrap(err,
×
947
                                        "failed to read form value 'type'",
×
948
                                )
×
949
                        }
×
950
                        msg.Type = string(b)
9✔
951

952
                case "size":
6✔
953
                        // Add size limit to the metadata
6✔
954
                        sz, err := io.ReadAll(part)
6✔
955
                        if err != nil {
6✔
UNCOV
956
                                return nil, err
×
957
                        }
×
958
                        size, err = strconv.ParseInt(string(sz), 10, 64)
6✔
959
                        if err != nil {
6✔
UNCOV
960
                                return nil, err
×
961
                        }
×
962
                        // Add one since this will impose the upper limit on the
963
                        // artifact size.
964
                        if size > d.config.MaxImageSize {
6✔
UNCOV
965
                                return nil, ErrModelArtifactFileTooLarge
×
966
                        }
×
967

UNCOV
968
                default:
×
969
                        // Ignore non-API sections.
×
970
                        continue
×
971
                }
972
        }
973

974
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
11✔
975
}
976

977
// deployments
978
func (d *DeploymentsApiHandlers) createDeployment(
979
        w rest.ResponseWriter,
980
        r *rest.Request,
981
        ctx context.Context,
982
        l *log.Logger,
983
        group string,
984
) {
13✔
985
        constructor, err := d.getDeploymentConstructorFromBody(r, group)
13✔
986
        if err != nil {
19✔
987
                d.view.RenderError(
6✔
988
                        w,
6✔
989
                        r,
6✔
990
                        errors.Wrap(err, "Validating request body"),
6✔
991
                        http.StatusBadRequest,
6✔
992
                        l,
6✔
993
                )
6✔
994
                return
6✔
995
        }
6✔
996

997
        id, err := d.app.CreateDeployment(ctx, constructor)
8✔
998
        switch err {
8✔
999
        case nil:
4✔
1000
                // in case of deployment to group remove "/group/{name}" from path before creating location
4✔
1001
                // haeder
4✔
1002
                r.URL.Path = strings.TrimSuffix(r.URL.Path, "/group/"+constructor.Group)
4✔
1003
                d.view.RenderSuccessPost(w, r, id)
4✔
1004
        case app.ErrNoArtifact:
1✔
1005
                d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
1✔
1006
        case app.ErrNoDevices:
2✔
1007
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1008
        default:
2✔
1009
                d.view.RenderInternalError(w, r, err, l)
2✔
1010
        }
1011
}
1012

1013
func (d *DeploymentsApiHandlers) PostDeployment(w rest.ResponseWriter, r *rest.Request) {
8✔
1014
        ctx := r.Context()
8✔
1015
        l := requestlog.GetRequestLogger(r)
8✔
1016

8✔
1017
        d.createDeployment(w, r, ctx, l, "")
8✔
1018
}
8✔
1019

1020
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
1021
        ctx := r.Context()
5✔
1022
        l := requestlog.GetRequestLogger(r)
5✔
1023

5✔
1024
        group := r.PathParam("name")
5✔
1025
        if len(group) < 1 {
5✔
UNCOV
1026
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
1027
        }
×
1028
        d.createDeployment(w, r, ctx, l, group)
5✔
1029
}
1030

1031
// parseDeviceConfigurationDeploymentPathParams parses expected params
1032
// and check if the params are not empty
1033
func parseDeviceConfigurationDeploymentPathParams(r *rest.Request) (string, string, string, error) {
7✔
1034
        tenantID := r.PathParam("tenant")
7✔
1035
        deviceID := r.PathParam(ParamDeviceID)
7✔
1036
        if deviceID == "" {
7✔
UNCOV
1037
                return "", "", "", errors.New("device ID missing")
×
1038
        }
×
1039
        deploymentID := r.PathParam(ParamDeploymentID)
7✔
1040
        if deploymentID == "" {
7✔
UNCOV
1041
                return "", "", "", errors.New("deployment ID missing")
×
1042
        }
×
1043
        return tenantID, deviceID, deploymentID, nil
7✔
1044
}
1045

1046
// getConfigurationDeploymentConstructorFromBody extracts configuration
1047
// deployment constructor from the request body and validates it
1048
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
1049
        *model.ConfigurationDeploymentConstructor, error) {
7✔
1050

7✔
1051
        var constructor *model.ConfigurationDeploymentConstructor
7✔
1052

7✔
1053
        if err := r.DecodeJsonPayload(&constructor); err != nil {
8✔
1054
                return nil, err
1✔
1055
        }
1✔
1056

1057
        if err := constructor.Validate(); err != nil {
8✔
1058
                return nil, err
2✔
1059
        }
2✔
1060

1061
        return constructor, nil
5✔
1062
}
1063

1064
// device configuration deployment handler
1065
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1066
        w rest.ResponseWriter,
1067
        r *rest.Request,
1068
) {
7✔
1069
        l := requestlog.GetRequestLogger(r)
7✔
1070

7✔
1071
        // get path params
7✔
1072
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
7✔
1073
        if err != nil {
7✔
UNCOV
1074
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
1075
                return
×
1076
        }
×
1077

1078
        // add tenant id to the context
1079
        ctx := identity.WithContext(r.Context(), &identity.Identity{Tenant: tenantID})
7✔
1080

7✔
1081
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
7✔
1082
        if err != nil {
10✔
1083
                d.view.RenderError(
3✔
1084
                        w,
3✔
1085
                        r,
3✔
1086
                        errors.Wrap(err, "Validating request body"),
3✔
1087
                        http.StatusBadRequest,
3✔
1088
                        l,
3✔
1089
                )
3✔
1090
                return
3✔
1091
        }
3✔
1092

1093
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
5✔
1094
        switch err {
5✔
1095
        default:
1✔
1096
                d.view.RenderInternalError(w, r, err, l)
1✔
1097
        case nil:
3✔
1098
                r.URL.Path = "./deployments"
3✔
1099
                d.view.RenderSuccessPost(w, r, id)
3✔
1100
        case app.ErrDuplicateDeployment:
2✔
1101
                d.view.RenderError(w, r, err, http.StatusConflict, l)
2✔
1102
        case app.ErrInvalidDeploymentID:
1✔
1103
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1104
        }
1105
}
1106

1107
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1108
        r *rest.Request,
1109
        group string,
1110
) (*model.DeploymentConstructor, error) {
13✔
1111
        var constructor *model.DeploymentConstructor
13✔
1112
        if err := r.DecodeJsonPayload(&constructor); err != nil {
16✔
1113
                return nil, err
3✔
1114
        }
3✔
1115

1116
        constructor.Group = group
11✔
1117

11✔
1118
        if err := constructor.ValidateNew(); err != nil {
15✔
1119
                return nil, err
4✔
1120
        }
4✔
1121

1122
        return constructor, nil
8✔
1123
}
1124

1125
func (d *DeploymentsApiHandlers) GetDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1126
        ctx := r.Context()
1✔
1127
        l := requestlog.GetRequestLogger(r)
1✔
1128

1✔
1129
        id := r.PathParam("id")
1✔
1130

1✔
1131
        if !govalidator.IsUUID(id) {
2✔
1132
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
1133
                return
1✔
1134
        }
1✔
1135

1136
        deployment, err := d.app.GetDeployment(ctx, id)
1✔
1137
        if err != nil {
1✔
UNCOV
1138
                d.view.RenderInternalError(w, r, err, l)
×
1139
                return
×
1140
        }
×
1141

1142
        if deployment == nil {
1✔
UNCOV
1143
                d.view.RenderErrorNotFound(w, r, l)
×
1144
                return
×
1145
        }
×
1146

1147
        d.view.RenderSuccessGet(w, deployment)
1✔
1148
}
1149

1150
func (d *DeploymentsApiHandlers) GetDeploymentStats(w rest.ResponseWriter, r *rest.Request) {
1✔
1151
        ctx := r.Context()
1✔
1152
        l := requestlog.GetRequestLogger(r)
1✔
1153

1✔
1154
        id := r.PathParam("id")
1✔
1155

1✔
1156
        if !govalidator.IsUUID(id) {
1✔
UNCOV
1157
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1158
                return
×
1159
        }
×
1160

1161
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1162
        if err != nil {
1✔
UNCOV
1163
                d.view.RenderInternalError(w, r, err, l)
×
1164
                return
×
1165
        }
×
1166

1167
        if stats == nil {
1✔
UNCOV
1168
                d.view.RenderErrorNotFound(w, r, l)
×
1169
                return
×
1170
        }
×
1171

1172
        d.view.RenderSuccessGet(w, stats)
1✔
1173
}
1174

1175
func (d *DeploymentsApiHandlers) GetDeploymentsStats(w rest.ResponseWriter, r *rest.Request) {
4✔
1176

4✔
1177
        ctx := r.Context()
4✔
1178
        l := requestlog.GetRequestLogger(r)
4✔
1179

4✔
1180
        ids := model.DeploymentIDs{}
4✔
1181
        if err := r.DecodeJsonPayload(&ids); err != nil {
4✔
UNCOV
1182
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1183
                return
×
1184
        }
×
1185

1186
        if len(ids.IDs) == 0 {
4✔
UNCOV
1187
                w.WriteHeader(http.StatusOK)
×
1188
                _ = w.WriteJson(struct{}{})
×
1189
                return
×
1190
        }
×
1191

1192
        if err := ids.Validate(); err != nil {
5✔
1193
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1194
                return
1✔
1195
        }
1✔
1196

1197
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
3✔
1198
        if err != nil {
5✔
1199
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
3✔
1200
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
1✔
1201
                        return
1✔
1202
                }
1✔
1203
                d.view.RenderInternalError(w, r, err, l)
1✔
1204
                return
1✔
1205
        }
1206

1207
        w.WriteHeader(http.StatusOK)
1✔
1208

1✔
1209
        _ = w.WriteJson(stats)
1✔
1210
}
1211

UNCOV
1212
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
1213
        ctx := r.Context()
×
1214
        l := requestlog.GetRequestLogger(r)
×
1215

×
1216
        id := r.PathParam("id")
×
1217

×
1218
        if !govalidator.IsUUID(id) {
×
1219
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1220
                return
×
1221
        }
×
1222

UNCOV
1223
        deployment, err := d.app.GetDeployment(ctx, id)
×
1224
        if err != nil {
×
1225
                d.view.RenderInternalError(w, r, err, l)
×
1226
                return
×
1227
        }
×
1228

UNCOV
1229
        if deployment == nil {
×
1230
                d.view.RenderErrorNotFound(w, r, l)
×
1231
                return
×
1232
        }
×
1233

UNCOV
1234
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1235
}
1236

1237
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1238
        ctx := r.Context()
1✔
1239
        l := requestlog.GetRequestLogger(r)
1✔
1240

1✔
1241
        id := r.PathParam("id")
1✔
1242

1✔
1243
        if !govalidator.IsUUID(id) {
1✔
UNCOV
1244
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1245
                return
×
1246
        }
×
1247

1248
        // receive request body
1249
        var status struct {
1✔
1250
                Status model.DeviceDeploymentStatus
1✔
1251
        }
1✔
1252

1✔
1253
        err := r.DecodeJsonPayload(&status)
1✔
1254
        if err != nil {
1✔
UNCOV
1255
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1256
                return
×
1257
        }
×
1258
        // "aborted" is the only supported status
1259
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
UNCOV
1260
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
1261
        }
×
1262

1263
        l.Infof("Abort deployment: %s", id)
1✔
1264

1✔
1265
        // Check if deployment is finished
1✔
1266
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1267
        if err != nil {
1✔
UNCOV
1268
                d.view.RenderInternalError(w, r, err, l)
×
1269
                return
×
1270
        }
×
1271
        if isDeploymentFinished {
2✔
1272
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
1✔
1273
                return
1✔
1274
        }
1✔
1275

1276
        // Abort deployments for devices and update deployment stats
1277
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
UNCOV
1278
                d.view.RenderInternalError(w, r, err, l)
×
1279
        }
×
1280

1281
        d.view.RenderEmptySuccessResponse(w)
1✔
1282
}
1283

1284
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
12✔
1285
        var (
12✔
1286
                installed *model.InstalledDeviceDeployment
12✔
1287
                ctx       = r.Context()
12✔
1288
                l         = requestlog.GetRequestLogger(r)
12✔
1289
                idata     = identity.FromContext(ctx)
12✔
1290
        )
12✔
1291
        if idata == nil {
14✔
1292
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
2✔
1293
                return
2✔
1294
        }
2✔
1295

1296
        q := r.URL.Query()
11✔
1297
        defer func() {
22✔
1298
                var reEncode bool = false
11✔
1299
                if name := q.Get(ParamArtifactName); name != "" {
20✔
1300
                        q.Set(ParamArtifactName, Redacted)
9✔
1301
                        reEncode = true
9✔
1302
                }
9✔
1303
                if typ := q.Get(ParamDeviceType); typ != "" {
20✔
1304
                        q.Set(ParamDeviceType, Redacted)
9✔
1305
                        reEncode = true
9✔
1306
                }
9✔
1307
                if reEncode {
20✔
1308
                        r.URL.RawQuery = q.Encode()
9✔
1309
                }
9✔
1310
        }()
1311
        if strings.EqualFold(r.Method, http.MethodPost) {
13✔
1312
                // POST
2✔
1313
                installed = new(model.InstalledDeviceDeployment)
2✔
1314
                if err := r.DecodeJsonPayload(&installed); err != nil {
3✔
1315
                        d.view.RenderError(w, r,
1✔
1316
                                errors.Wrap(err, "invalid schema"),
1✔
1317
                                http.StatusBadRequest, l)
1✔
1318
                        return
1✔
1319
                }
1✔
1320
        } else {
9✔
1321
                // GET or HEAD
9✔
1322
                installed = &model.InstalledDeviceDeployment{
9✔
1323
                        ArtifactName: q.Get(ParamArtifactName),
9✔
1324
                        DeviceType:   q.Get(ParamDeviceType),
9✔
1325
                }
9✔
1326
        }
9✔
1327

1328
        if err := installed.Validate(); err != nil {
11✔
1329
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1330
                return
1✔
1331
        }
1✔
1332

1333
        request := &model.DeploymentNextRequest{
9✔
1334
                DeviceProvides: installed,
9✔
1335
        }
9✔
1336

9✔
1337
        d.getDeploymentForDevice(w, r, idata, request)
9✔
1338
}
1339

1340
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1341
        w rest.ResponseWriter,
1342
        r *rest.Request,
1343
        idata *identity.Identity,
1344
        request *model.DeploymentNextRequest,
1345
) {
9✔
1346
        ctx := r.Context()
9✔
1347
        l := requestlog.GetRequestLogger(r)
9✔
1348

9✔
1349
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
9✔
1350
        if err != nil {
11✔
1351
                if err == app.ErrConflictingRequestData {
3✔
1352
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1353
                } else {
2✔
1354
                        d.view.RenderInternalError(w, r, err, l)
1✔
1355
                }
1✔
1356
                return
2✔
1357
        }
1358

1359
        if deployment == nil {
10✔
1360
                d.view.RenderNoUpdateForDevice(w)
2✔
1361
                return
2✔
1362
        } else if deployment.Type == model.DeploymentTypeConfiguration {
14✔
1363
                // Generate pre-signed URL
5✔
1364
                var hostName string = d.config.PresignHostname
5✔
1365
                if hostName == "" {
7✔
1366
                        if hostName = r.Header.Get(hdrForwardedHost); hostName == "" {
3✔
1367
                                d.view.RenderInternalError(w, r,
1✔
1368
                                        errors.New("presign.hostname not configured; "+
1✔
1369
                                                "unable to generate download link "+
1✔
1370
                                                " for configuration deployment"), l)
1✔
1371
                                return
1✔
1372
                        }
1✔
1373
                }
1374
                req, _ := http.NewRequest(
4✔
1375
                        http.MethodGet,
4✔
1376
                        FMTConfigURL(
4✔
1377
                                d.config.PresignScheme, hostName,
4✔
1378
                                deployment.ID, request.DeviceProvides.DeviceType,
4✔
1379
                                idata.Subject,
4✔
1380
                        ),
4✔
1381
                        nil,
4✔
1382
                )
4✔
1383
                if idata.Tenant != "" {
7✔
1384
                        q := req.URL.Query()
3✔
1385
                        q.Set(model.ParamTenantID, idata.Tenant)
3✔
1386
                        req.URL.RawQuery = q.Encode()
3✔
1387
                }
3✔
1388
                sig := model.NewRequestSignature(req, d.config.PresignSecret)
4✔
1389
                expireTS := time.Now().Add(d.config.PresignExpire)
4✔
1390
                sig.SetExpire(expireTS)
4✔
1391
                deployment.Artifact.Source = model.Link{
4✔
1392
                        Uri:    sig.PresignURL(),
4✔
1393
                        Expire: expireTS,
4✔
1394
                }
4✔
1395
        }
1396

1397
        d.view.RenderSuccessGet(w, deployment)
6✔
1398
}
1399

1400
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1401
        w rest.ResponseWriter,
1402
        r *rest.Request,
1403
) {
1✔
1404
        ctx := r.Context()
1✔
1405
        l := requestlog.GetRequestLogger(r)
1✔
1406

1✔
1407
        did := r.PathParam("id")
1✔
1408

1✔
1409
        idata := identity.FromContext(ctx)
1✔
1410
        if idata == nil {
1✔
UNCOV
1411
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1412
                return
×
1413
        }
×
1414

1415
        // receive request body
1416
        var report model.StatusReport
1✔
1417

1✔
1418
        err := r.DecodeJsonPayload(&report)
1✔
1419
        if err != nil {
1✔
UNCOV
1420
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1421
                return
×
1422
        }
×
1423

1424
        l.Infof("status: %+v", report)
1✔
1425
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
1✔
1426
                idata.Subject, model.DeviceDeploymentState{
1✔
1427
                        Status:   report.Status,
1✔
1428
                        SubState: report.SubState,
1✔
1429
                }); err != nil {
1✔
UNCOV
1430

×
1431
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
1432
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
1433
                } else if err == app.ErrStorageNotFound {
×
1434
                        d.view.RenderErrorNotFound(w, r, l)
×
1435
                } else {
×
1436
                        d.view.RenderInternalError(w, r, err, l)
×
1437
                }
×
1438
                return
×
1439
        }
1440

1441
        d.view.RenderEmptySuccessResponse(w)
1✔
1442
}
1443

1444
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1445
        w rest.ResponseWriter,
1446
        r *rest.Request,
1447
) {
1✔
1448
        ctx := r.Context()
1✔
1449
        l := requestlog.GetRequestLogger(r)
1✔
1450

1✔
1451
        did := r.PathParam("id")
1✔
1452

1✔
1453
        if !govalidator.IsUUID(did) {
1✔
UNCOV
1454
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1455
                return
×
1456
        }
×
1457

1458
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
1✔
1459
        if err != nil {
1✔
UNCOV
1460
                switch err {
×
1461
                case app.ErrModelDeploymentNotFound:
×
1462
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1463
                        return
×
1464
                default:
×
1465
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1466
                        return
×
1467
                }
1468
        }
1469

1470
        d.view.RenderSuccessGet(w, statuses)
1✔
1471
}
1472

1473
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1474
        w rest.ResponseWriter,
1475
        r *rest.Request,
1476
) {
1✔
1477
        ctx := r.Context()
1✔
1478
        l := requestlog.GetRequestLogger(r)
1✔
1479

1✔
1480
        did := r.PathParam("id")
1✔
1481

1✔
1482
        if !govalidator.IsUUID(did) {
1✔
UNCOV
1483
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1484
                return
×
1485
        }
×
1486

1487
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1488
        if err != nil {
1✔
UNCOV
1489
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1490
                return
×
1491
        }
×
1492

1493
        lq := store.ListQuery{
1✔
1494
                Skip:         int((page - 1) * perPage),
1✔
1495
                Limit:        int(perPage),
1✔
1496
                DeploymentID: did,
1✔
1497
        }
1✔
1498
        if status := r.URL.Query().Get("status"); status != "" {
1✔
UNCOV
1499
                lq.Status = &status
×
1500
        }
×
1501
        if err = lq.Validate(); err != nil {
1✔
UNCOV
1502
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1503
                return
×
1504
        }
×
1505

1506
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1507
        if err != nil {
1✔
UNCOV
1508
                switch err {
×
1509
                case app.ErrModelDeploymentNotFound:
×
1510
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1511
                        return
×
1512
                default:
×
1513
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1514
                        return
×
1515
                }
1516
        }
1517

1518
        hasNext := totalCount > int(page*perPage)
1✔
1519
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1520
        for _, l := range links {
2✔
1521
                w.Header().Add("Link", l)
1✔
1522
        }
1✔
1523
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1524
        d.view.RenderSuccessGet(w, statuses)
1✔
1525
}
1526

1527
func ParseLookupQuery(vals url.Values) (model.Query, error) {
9✔
1528
        query := model.Query{}
9✔
1529

9✔
1530
        search := vals.Get("search")
9✔
1531
        if search != "" {
9✔
UNCOV
1532
                query.SearchText = search
×
1533
        }
×
1534

1535
        createdBefore := vals.Get("created_before")
9✔
1536
        if createdBefore != "" {
10✔
1537
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
2✔
1538
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1539
                } else {
1✔
UNCOV
1540
                        query.CreatedBefore = &createdBeforeTime
×
1541
                }
×
1542
        }
1543

1544
        createdAfter := vals.Get("created_after")
8✔
1545
        if createdAfter != "" {
8✔
UNCOV
1546
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1547
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1548
                } else {
×
1549
                        query.CreatedAfter = &createdAfterTime
×
1550
                }
×
1551
        }
1552

1553
        switch strings.ToLower(vals.Get("sort")) {
8✔
1554
        case model.SortDirectionAscending:
1✔
1555
                query.Sort = model.SortDirectionAscending
1✔
1556
        case "", model.SortDirectionDescending:
7✔
1557
                query.Sort = model.SortDirectionDescending
7✔
UNCOV
1558
        default:
×
1559
                return query, ErrInvalidSortDirection
×
1560
        }
1561

1562
        status := vals.Get("status")
8✔
1563
        switch status {
8✔
UNCOV
1564
        case "inprogress":
×
1565
                query.Status = model.StatusQueryInProgress
×
1566
        case "finished":
×
1567
                query.Status = model.StatusQueryFinished
×
1568
        case "pending":
×
1569
                query.Status = model.StatusQueryPending
×
1570
        case "aborted":
×
1571
                query.Status = model.StatusQueryAborted
×
1572
        case "":
8✔
1573
                query.Status = model.StatusQueryAny
8✔
UNCOV
1574
        default:
×
1575
                return query, errors.Errorf("unknown status %s", status)
×
1576

1577
        }
1578

1579
        dType := vals.Get("type")
8✔
1580
        if dType == "" {
16✔
1581
                return query, nil
8✔
1582
        }
8✔
UNCOV
1583
        deploymentType := model.DeploymentType(dType)
×
1584
        if deploymentType == model.DeploymentTypeSoftware ||
×
1585
                deploymentType == model.DeploymentTypeConfiguration {
×
1586
                query.Type = deploymentType
×
1587
        } else {
×
1588
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1589
        }
×
1590

UNCOV
1591
        return query, nil
×
1592
}
1593

1594
func parseEpochToTimestamp(epoch string) (time.Time, error) {
1✔
1595
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
2✔
1596
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
1✔
1597
        } else {
1✔
UNCOV
1598
                return time.Unix(epochInt64, 0).UTC(), nil
×
1599
        }
×
1600
}
1601

1602
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
9✔
1603
        ctx := r.Context()
9✔
1604
        l := requestlog.GetRequestLogger(r)
9✔
1605
        q := r.URL.Query()
9✔
1606
        defer func() {
18✔
1607
                if search := q.Get("search"); search != "" {
9✔
UNCOV
1608
                        q.Set("search", Redacted)
×
1609
                        r.URL.RawQuery = q.Encode()
×
1610
                }
×
1611
        }()
1612

1613
        query, err := ParseLookupQuery(q)
9✔
1614
        if err != nil {
10✔
1615
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1616
                return
1✔
1617
        }
1✔
1618

1619
        page, perPage, err := rest_utils.ParsePagination(r)
8✔
1620
        if err != nil {
9✔
1621
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1622
                return
1✔
1623
        }
1✔
1624
        query.Skip = int((page - 1) * perPage)
7✔
1625
        query.Limit = int(perPage + 1)
7✔
1626

7✔
1627
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
7✔
1628
        if err != nil {
8✔
1629
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1630
                return
1✔
1631
        }
1✔
1632
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
6✔
1633

6✔
1634
        len := len(deps)
6✔
1635
        hasNext := false
6✔
1636
        if uint64(len) > perPage {
6✔
UNCOV
1637
                hasNext = true
×
1638
                len = int(perPage)
×
1639
        }
×
1640

1641
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
6✔
1642
        for _, l := range links {
13✔
1643
                w.Header().Add("Link", l)
7✔
1644
        }
7✔
1645

1646
        d.view.RenderSuccessGet(w, deps[:len])
6✔
1647
}
1648

1649
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1650
        ctx := r.Context()
1✔
1651
        l := requestlog.GetRequestLogger(r)
1✔
1652

1✔
1653
        did := r.PathParam("id")
1✔
1654

1✔
1655
        idata := identity.FromContext(ctx)
1✔
1656
        if idata == nil {
1✔
UNCOV
1657
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1658
                return
×
1659
        }
×
1660

1661
        // reuse DeploymentLog, device and deployment IDs are ignored when
1662
        // (un-)marshaling DeploymentLog to/from JSON
1663
        var log model.DeploymentLog
1✔
1664

1✔
1665
        err := r.DecodeJsonPayload(&log)
1✔
1666
        if err != nil {
1✔
UNCOV
1667
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1668
                return
×
1669
        }
×
1670

1671
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1672
                did, log.Messages); err != nil {
1✔
UNCOV
1673

×
1674
                if err == app.ErrModelDeploymentNotFound {
×
1675
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1676
                } else {
×
1677
                        d.view.RenderInternalError(w, r, err, l)
×
1678
                }
×
1679
                return
×
1680
        }
1681

1682
        d.view.RenderEmptySuccessResponse(w)
1✔
1683
}
1684

1685
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1686
        ctx := r.Context()
1✔
1687
        l := requestlog.GetRequestLogger(r)
1✔
1688

1✔
1689
        did := r.PathParam("id")
1✔
1690
        devid := r.PathParam("devid")
1✔
1691

1✔
1692
        depl, err := d.app.GetDeviceDeploymentLog(ctx, devid, did)
1✔
1693

1✔
1694
        if err != nil {
1✔
UNCOV
1695
                d.view.RenderInternalError(w, r, err, l)
×
1696
                return
×
1697
        }
×
1698

1699
        if depl == nil {
1✔
UNCOV
1700
                d.view.RenderErrorNotFound(w, r, l)
×
1701
                return
×
1702
        }
×
1703

1704
        d.view.RenderDeploymentLog(w, *depl)
1✔
1705
}
1706

1707
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
3✔
1708
        ctx := r.Context()
3✔
1709
        l := requestlog.GetRequestLogger(r)
3✔
1710

3✔
1711
        id := r.PathParam("id")
3✔
1712
        err := d.app.AbortDeviceDeployments(ctx, id)
3✔
1713

3✔
1714
        switch err {
3✔
1715
        case nil, app.ErrStorageNotFound:
2✔
1716
                d.view.RenderEmptySuccessResponse(w)
2✔
1717
        default:
1✔
1718
                d.view.RenderInternalError(w, r, err, l)
1✔
1719
        }
1720
}
1721

1722
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1723
        r *rest.Request) {
3✔
1724
        ctx := r.Context()
3✔
1725
        l := requestlog.GetRequestLogger(r)
3✔
1726

3✔
1727
        id := r.PathParam("id")
3✔
1728
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
3✔
1729

3✔
1730
        switch err {
3✔
1731
        case nil, app.ErrStorageNotFound:
2✔
1732
                d.view.RenderEmptySuccessResponse(w)
2✔
1733
        default:
1✔
1734
                d.view.RenderInternalError(w, r, err, l)
1✔
1735
        }
1736
}
1737

1738
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
9✔
1739
        ctx := r.Context()
9✔
1740
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1741
}
9✔
1742

1743
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1744
        r *rest.Request) {
9✔
1745
        ctx := r.Context()
9✔
1746
        tenantID := r.PathParam("tenant")
9✔
1747
        if tenantID != "" {
18✔
1748
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1749
                        Tenant:   tenantID,
9✔
1750
                        IsDevice: true,
9✔
1751
                })
9✔
1752
        }
9✔
1753
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1754
}
1755

1756
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(w rest.ResponseWriter,
1757
        r *rest.Request) {
9✔
1758
        ctx := r.Context()
9✔
1759
        tenantID := r.PathParam("tenant")
9✔
1760
        if tenantID != "" {
18✔
1761
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1762
                        Tenant:   tenantID,
9✔
1763
                        IsDevice: true,
9✔
1764
                })
9✔
1765
        }
9✔
1766
        d.listDeviceDeployments(ctx, w, r, false)
9✔
1767
}
1768

1769
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1770
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
27✔
1771
        l := requestlog.GetRequestLogger(r)
27✔
1772

27✔
1773
        did := ""
27✔
1774
        var IDs []string
27✔
1775
        if byDeviceID {
45✔
1776
                did = r.PathParam("id")
18✔
1777
        } else {
27✔
1778
                values := r.URL.Query()
9✔
1779
                if values.Has("id") && len(values["id"]) > 0 {
17✔
1780
                        IDs = values["id"]
8✔
1781
                } else {
9✔
1782
                        d.view.RenderError(w, r, ErrEmptyID, http.StatusBadRequest, l)
1✔
1783
                        return
1✔
1784
                }
1✔
1785
        }
1786

1787
        page, perPage, err := rest_utils.ParsePagination(r)
26✔
1788
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
29✔
1789
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
3✔
1790
        }
3✔
1791
        if err != nil {
32✔
1792
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
6✔
1793
                return
6✔
1794
        }
6✔
1795

1796
        lq := store.ListQueryDeviceDeployments{
20✔
1797
                Skip:     int((page - 1) * perPage),
20✔
1798
                Limit:    int(perPage),
20✔
1799
                DeviceID: did,
20✔
1800
                IDs:      IDs,
20✔
1801
        }
20✔
1802
        if status := r.URL.Query().Get("status"); status != "" {
26✔
1803
                lq.Status = &status
6✔
1804
        }
6✔
1805
        if err = lq.Validate(); err != nil {
23✔
1806
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
3✔
1807
                return
3✔
1808
        }
3✔
1809

1810
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
17✔
1811
        if err != nil {
20✔
1812
                d.view.RenderInternalError(w, r, err, l)
3✔
1813
                return
3✔
1814
        }
3✔
1815
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
14✔
1816

14✔
1817
        hasNext := totalCount > lq.Skip+len(deps)
14✔
1818
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
14✔
1819
        for _, l := range links {
28✔
1820
                w.Header().Add("Link", l)
14✔
1821
        }
14✔
1822

1823
        d.view.RenderSuccessGet(w, deps)
14✔
1824
}
1825

1826
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
UNCOV
1827
        r *rest.Request) {
×
1828
        ctx := r.Context()
×
1829
        tenantID := r.PathParam("tenantID")
×
1830
        if tenantID != "" {
×
1831
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1832
                        Tenant:   tenantID,
×
1833
                        IsDevice: true,
×
1834
                })
×
1835
        }
×
1836

UNCOV
1837
        l := requestlog.GetRequestLogger(r)
×
1838

×
1839
        id := r.PathParam("id")
×
1840

×
1841
        // Decommission deployments for devices and update deployment stats
×
1842
        err := d.app.DecommissionDevice(ctx, id)
×
1843

×
1844
        switch err {
×
1845
        case nil, app.ErrStorageNotFound:
×
1846
                d.view.RenderEmptySuccessResponse(w)
×
1847
        default:
×
1848
                d.view.RenderInternalError(w, r, err, l)
×
1849

1850
        }
1851
}
1852

1853
// tenants
1854

1855
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1856
        ctx := r.Context()
1✔
1857
        l := requestlog.GetRequestLogger(r)
1✔
1858

1✔
1859
        defer r.Body.Close()
1✔
1860

1✔
1861
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1862
        if err != nil {
2✔
1863
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1864
                return
1✔
1865
        }
1✔
1866

1867
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1868
        if err != nil {
1✔
UNCOV
1869
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1870
                return
×
1871
        }
×
1872

1873
        w.WriteHeader(http.StatusCreated)
1✔
1874
}
1875

1876
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1877
        w rest.ResponseWriter,
1878
        r *rest.Request,
1879
) {
6✔
1880
        tenantID := r.PathParam("tenant")
6✔
1881
        if tenantID == "" {
7✔
1882
                l := requestlog.GetRequestLogger(r)
1✔
1883
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1884
                return
1✔
1885
        }
1✔
1886

1887
        r.Request = r.WithContext(identity.WithContext(
5✔
1888
                r.Context(),
5✔
1889
                &identity.Identity{Tenant: tenantID},
5✔
1890
        ))
5✔
1891
        d.LookupDeployment(w, r)
5✔
1892
}
1893

1894
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1895
        w rest.ResponseWriter,
1896
        r *rest.Request,
1897
) {
5✔
1898
        l := requestlog.GetRequestLogger(r)
5✔
1899

5✔
1900
        tenantID := r.PathParam("tenant")
5✔
1901

5✔
1902
        ctx := identity.WithContext(
5✔
1903
                r.Context(),
5✔
1904
                &identity.Identity{Tenant: tenantID},
5✔
1905
        )
5✔
1906

5✔
1907
        settings, err := d.app.GetStorageSettings(ctx)
5✔
1908
        if err != nil {
7✔
1909
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1910
                return
2✔
1911
        }
2✔
1912

1913
        d.view.RenderSuccessGet(w, settings)
3✔
1914
}
1915

1916
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1917
        w rest.ResponseWriter,
1918
        r *rest.Request,
1919
) {
10✔
1920
        l := requestlog.GetRequestLogger(r)
10✔
1921

10✔
1922
        defer r.Body.Close()
10✔
1923

10✔
1924
        tenantID := r.PathParam("tenant")
10✔
1925

10✔
1926
        ctx := identity.WithContext(
10✔
1927
                r.Context(),
10✔
1928
                &identity.Identity{Tenant: tenantID},
10✔
1929
        )
10✔
1930

10✔
1931
        settings, err := model.ParseStorageSettingsRequest(r.Body)
10✔
1932
        if err != nil {
13✔
1933
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
1934
                return
3✔
1935
        }
3✔
1936

1937
        err = d.app.SetStorageSettings(ctx, settings)
8✔
1938
        if err != nil {
10✔
1939
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1940
                return
2✔
1941
        }
2✔
1942

1943
        w.WriteHeader(http.StatusNoContent)
6✔
1944
}
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