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

mendersoftware / deployments / 929748838

pending completion
929748838

Pull #884

gitlab-ci

merlin-northern
test: db units for update release.

Ticket: MEN-6593
Signed-off-by: Peter Grzybowski <peter@northern.tech>
Pull Request #884: feat: release notes: update and store.

268 of 340 new or added lines in 6 files covered. (78.82%)

415 existing lines in 5 files now uncovered.

7650 of 9637 relevant lines covered (79.38%)

33.69 hits per line

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

76.49
/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
        "fmt"
20
        "io"
21
        "mime/multipart"
22
        "net/http"
23
        "net/url"
24
        "strconv"
25
        "strings"
26
        "time"
27

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

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

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

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

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

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

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

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

85
const Redacted = "REDACTED"
86

87
// JWT token
88
const (
89
        HTTPHeaderAuthorization       = "Authorization"
90
        HTTPHeaderAuthorizationBearer = "Bearer"
91
)
92

93
const (
94
        defaultTimeout = time.Second * 10
95
)
96

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

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

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

126
type Config struct {
127
        // URL signing parameters:
128

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

140
        EnableDirectUpload bool
141
        // EnableDirectUploadSkipVerify allows turning off the verification of uploaded artifacts
142
        EnableDirectUploadSkipVerify bool
143
}
144

145
func NewConfig() *Config {
199✔
146
        return &Config{
199✔
147
                PresignExpire: DefaultDownloadLinkExpire,
199✔
148
                PresignScheme: "https",
199✔
149
                MaxImageSize:  DefaultMaxImageSize,
199✔
150
        }
199✔
151
}
199✔
152

153
func (conf *Config) SetPresignSecret(key []byte) *Config {
20✔
154
        conf.PresignSecret = key
20✔
155
        return conf
20✔
156
}
20✔
157

158
func (conf *Config) SetPresignExpire(duration time.Duration) *Config {
14✔
159
        conf.PresignExpire = duration
14✔
160
        return conf
14✔
161
}
14✔
162

163
func (conf *Config) SetPresignHostname(hostname string) *Config {
12✔
164
        conf.PresignHostname = hostname
12✔
165
        return conf
12✔
166
}
12✔
167

168
func (conf *Config) SetPresignScheme(scheme string) *Config {
14✔
169
        conf.PresignScheme = scheme
14✔
170
        return conf
14✔
171
}
14✔
172

173
func (conf *Config) SetMaxImageSize(size int64) *Config {
1✔
174
        conf.MaxImageSize = size
1✔
175
        return conf
1✔
176
}
1✔
177

178
func (conf *Config) SetEnableDirectUpload(enable bool) *Config {
12✔
179
        conf.EnableDirectUpload = enable
12✔
180
        return conf
12✔
181
}
12✔
182

183
func (conf *Config) SetEnableDirectUploadSkipVerify(enable bool) *Config {
1✔
184
        conf.EnableDirectUploadSkipVerify = enable
1✔
185
        return conf
1✔
186
}
1✔
187

188
type DeploymentsApiHandlers struct {
189
        view   RESTView
190
        store  store.DataStore
191
        app    app.App
192
        config Config
193
}
194

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

232
func (d *DeploymentsApiHandlers) AliveHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
233
        w.WriteHeader(http.StatusNoContent)
1✔
234
}
1✔
235

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

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

250
func getReleaseOrImageFilter(r *rest.Request, paginated bool) *model.ReleaseOrImageFilter {
25✔
251

25✔
252
        q := r.URL.Query()
25✔
253

25✔
254
        filter := &model.ReleaseOrImageFilter{
25✔
255
                Name:        q.Get(ParamName),
25✔
256
                Description: q.Get(ParamDescription),
25✔
257
                DeviceType:  q.Get(ParamDeviceType),
25✔
258
        }
25✔
259

25✔
260
        if paginated {
37✔
261
                filter.Sort = q.Get(ParamSort)
12✔
262
                if page := q.Get(ParamPage); page != "" {
13✔
263
                        if i, err := strconv.Atoi(page); err == nil {
2✔
264
                                filter.Page = i
1✔
265
                        }
1✔
266
                }
267
                if perPage := q.Get(ParamPerPage); perPage != "" {
14✔
268
                        if i, err := strconv.Atoi(perPage); err == nil {
4✔
269
                                filter.PerPage = i
2✔
270
                        }
2✔
271
                }
272
                if filter.Page <= 0 {
23✔
273
                        filter.Page = 1
11✔
274
                }
11✔
275
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
23✔
276
                        filter.PerPage = DefaultPerPage
11✔
277
                }
11✔
278
        }
279

280
        return filter
25✔
281
}
282

283
type limitResponse struct {
284
        Limit uint64 `json:"limit"`
285
        Usage uint64 `json:"usage"`
286
}
287

288
func (d *DeploymentsApiHandlers) GetLimit(w rest.ResponseWriter, r *rest.Request) {
3✔
289
        l := requestlog.GetRequestLogger(r)
3✔
290

3✔
291
        name := r.PathParam("name")
3✔
292

3✔
293
        if !model.IsValidLimit(name) {
4✔
294
                d.view.RenderError(w, r,
1✔
295
                        errors.Errorf("unsupported limit %s", name),
1✔
296
                        http.StatusBadRequest, l)
1✔
297
                return
1✔
298
        }
1✔
299

300
        limit, err := d.app.GetLimit(r.Context(), name)
2✔
301
        if err != nil {
3✔
302
                d.view.RenderInternalError(w, r, err, l)
1✔
303
                return
1✔
304
        }
1✔
305

306
        d.view.RenderSuccessGet(w, limitResponse{
1✔
307
                Limit: limit.Value,
1✔
308
                Usage: 0, // TODO fill this when ready
1✔
309
        })
1✔
310
}
311

312
// images
313

314
func (d *DeploymentsApiHandlers) GetImage(w rest.ResponseWriter, r *rest.Request) {
1✔
315
        l := requestlog.GetRequestLogger(r)
1✔
316

1✔
317
        id := r.PathParam("id")
1✔
318

1✔
319
        if !govalidator.IsUUID(id) {
2✔
320
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
321
                return
1✔
322
        }
1✔
323

324
        image, err := d.app.GetImage(r.Context(), id)
1✔
325
        if err != nil {
1✔
326
                d.view.RenderInternalError(w, r, err, l)
×
327
                return
×
328
        }
×
329

330
        if image == nil {
2✔
331
                d.view.RenderErrorNotFound(w, r, l)
1✔
332
                return
1✔
333
        }
1✔
334

335
        d.view.RenderSuccessGet(w, image)
1✔
336
}
337

338
func (d *DeploymentsApiHandlers) GetImages(w rest.ResponseWriter, r *rest.Request) {
5✔
339
        l := requestlog.GetRequestLogger(r)
5✔
340

5✔
341
        defer redactReleaseName(r)
5✔
342
        filter := getReleaseOrImageFilter(r, false)
5✔
343

5✔
344
        list, _, err := d.app.ListImages(r.Context(), filter)
5✔
345
        if err != nil {
6✔
346
                d.view.RenderInternalError(w, r, err, l)
1✔
347
                return
1✔
348
        }
1✔
349

350
        d.view.RenderSuccessGet(w, list)
4✔
351
}
352

353
func (d *DeploymentsApiHandlers) ListImages(w rest.ResponseWriter, r *rest.Request) {
4✔
354
        l := requestlog.GetRequestLogger(r)
4✔
355

4✔
356
        defer redactReleaseName(r)
4✔
357
        filter := getReleaseOrImageFilter(r, true)
4✔
358

4✔
359
        list, totalCount, err := d.app.ListImages(r.Context(), filter)
4✔
360
        if err != nil {
5✔
361
                d.view.RenderInternalError(w, r, err, l)
1✔
362
                return
1✔
363
        }
1✔
364

365
        hasNext := totalCount > int(filter.Page*filter.PerPage)
3✔
366
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
3✔
367
        for _, l := range links {
6✔
368
                w.Header().Add("Link", l)
3✔
369
        }
3✔
370
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
371

3✔
372
        d.view.RenderSuccessGet(w, list)
3✔
373
}
374

375
func (d *DeploymentsApiHandlers) DownloadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
376
        l := requestlog.GetRequestLogger(r)
1✔
377

1✔
378
        id := r.PathParam("id")
1✔
379

1✔
380
        if !govalidator.IsUUID(id) {
1✔
381
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
382
                return
×
383
        }
×
384

385
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
386
        link, err := d.app.DownloadLink(r.Context(), id, time.Duration(expireSeconds)*time.Second)
1✔
387
        if err != nil {
1✔
388
                d.view.RenderInternalError(w, r, err, l)
×
389
                return
×
390
        }
×
391

392
        if link == nil {
1✔
393
                d.view.RenderErrorNotFound(w, r, l)
×
394
                return
×
395
        }
×
396

397
        d.view.RenderSuccessGet(w, link)
1✔
398
}
399

400
func (d *DeploymentsApiHandlers) UploadLink(w rest.ResponseWriter, r *rest.Request) {
4✔
401
        l := requestlog.GetRequestLogger(r)
4✔
402

4✔
403
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
4✔
404
        link, err := d.app.UploadLink(
4✔
405
                r.Context(),
4✔
406
                time.Duration(expireSeconds)*time.Second,
4✔
407
                d.config.EnableDirectUploadSkipVerify,
4✔
408
        )
4✔
409
        if err != nil {
5✔
410
                d.view.RenderInternalError(w, r, err, l)
1✔
411
                return
1✔
412
        }
1✔
413

414
        if link == nil {
4✔
415
                d.view.RenderErrorNotFound(w, r, l)
1✔
416
                return
1✔
417
        }
1✔
418

419
        d.view.RenderSuccessGet(w, link)
2✔
420
}
421

422
func (d *DeploymentsApiHandlers) CompleteUpload(w rest.ResponseWriter, r *rest.Request) {
4✔
423
        ctx := r.Context()
4✔
424
        l := log.FromContext(ctx)
4✔
425

4✔
426
        artifactID := r.PathParam(ParamID)
4✔
427

4✔
428
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify)
4✔
429
        switch errors.Cause(err) {
4✔
430
        case nil:
2✔
431
                // w.Header().Set("Link", "FEAT: Upload status API")
2✔
432
                w.WriteHeader(http.StatusAccepted)
2✔
433
        case app.ErrUploadNotFound:
1✔
434
                d.view.RenderErrorNotFound(w, r, l)
1✔
435
        default:
1✔
436
                l.Error(err)
1✔
437
                w.WriteHeader(http.StatusInternalServerError)
1✔
438
                w.WriteJson(rest_utils.ApiError{ // nolint:errcheck
1✔
439
                        Err:   "internal server error",
1✔
440
                        ReqId: requestid.FromContext(ctx),
1✔
441
                })
1✔
442
        }
443
}
444

445
func (d *DeploymentsApiHandlers) DownloadConfiguration(w rest.ResponseWriter, r *rest.Request) {
10✔
446
        if d.config.PresignSecret == nil {
11✔
447
                rest.NotFound(w, r)
1✔
448
                return
1✔
449
        }
1✔
450
        var (
9✔
451
                deviceID, _     = url.PathUnescape(r.PathParam(ParamDeviceID))
9✔
452
                deviceType, _   = url.PathUnescape(r.PathParam(ParamDeviceType))
9✔
453
                deploymentID, _ = url.PathUnescape(r.PathParam(ParamDeploymentID))
9✔
454
        )
9✔
455
        if deviceID == "" || deviceType == "" || deploymentID == "" {
9✔
456
                rest.NotFound(w, r)
×
457
                return
×
458
        }
×
459

460
        var (
9✔
461
                tenantID string
9✔
462
                l        = log.FromContext(r.Context())
9✔
463
                q        = r.URL.Query()
9✔
464
                err      error
9✔
465
        )
9✔
466
        tenantID = q.Get(ParamTenantID)
9✔
467
        sig := model.NewRequestSignature(r.Request, d.config.PresignSecret)
9✔
468
        if err = sig.Validate(); err != nil {
12✔
469
                switch cause := errors.Cause(err); cause {
3✔
470
                case model.ErrLinkExpired:
1✔
471
                        d.view.RenderError(w, r, cause, http.StatusForbidden, l)
1✔
472
                default:
2✔
473
                        d.view.RenderError(w, r,
2✔
474
                                errors.Wrap(err, "invalid request parameters"),
2✔
475
                                http.StatusBadRequest, l,
2✔
476
                        )
2✔
477
                }
478
                return
3✔
479
        }
480

481
        if !sig.VerifyHMAC256() {
9✔
482
                d.view.RenderError(w, r,
2✔
483
                        errors.New("signature invalid"),
2✔
484
                        http.StatusForbidden, l,
2✔
485
                )
2✔
486
                return
2✔
487
        }
2✔
488

489
        // Validate request signature
490
        ctx := identity.WithContext(r.Context(), &identity.Identity{
6✔
491
                Subject:  deviceID,
6✔
492
                Tenant:   tenantID,
6✔
493
                IsDevice: true,
6✔
494
        })
6✔
495

6✔
496
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
6✔
497
        if err != nil {
8✔
498
                switch cause := errors.Cause(err); cause {
2✔
499
                case app.ErrModelDeploymentNotFound:
1✔
500
                        d.view.RenderError(w, r,
1✔
501
                                errors.Errorf(
1✔
502
                                        "deployment with id '%s' not found",
1✔
503
                                        deploymentID,
1✔
504
                                ),
1✔
505
                                http.StatusNotFound, l,
1✔
506
                        )
1✔
507
                default:
1✔
508
                        l.Error(err.Error())
1✔
509
                        d.view.RenderInternalError(w, r, err, l)
1✔
510
                }
511
                return
2✔
512
        }
513
        artifactPayload, err := io.ReadAll(artifact)
4✔
514
        if err != nil {
5✔
515
                l.Error(err.Error())
1✔
516
                d.view.RenderInternalError(w, r, err, l)
1✔
517
                return
1✔
518
        }
1✔
519

520
        rw := w.(http.ResponseWriter)
3✔
521
        hdr := rw.Header()
3✔
522
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
3✔
523
        hdr.Set("Content-Type", app.ArtifactContentType)
3✔
524
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
3✔
525
        rw.WriteHeader(http.StatusOK)
3✔
526
        _, err = rw.Write(artifactPayload)
3✔
527
        if err != nil {
3✔
528
                // There's not anything we can do here in terms of the response.
×
529
                l.Error(err.Error())
×
530
        }
×
531
}
532

533
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
1✔
534
        l := requestlog.GetRequestLogger(r)
1✔
535

1✔
536
        id := r.PathParam("id")
1✔
537

1✔
538
        if !govalidator.IsUUID(id) {
1✔
539
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
540
                return
×
541
        }
×
542

543
        if err := d.app.DeleteImage(r.Context(), id); err != nil {
2✔
544
                switch err {
1✔
545
                default:
×
546
                        d.view.RenderInternalError(w, r, err, l)
×
547
                case app.ErrImageMetaNotFound:
×
548
                        d.view.RenderErrorNotFound(w, r, l)
×
549
                case app.ErrModelImageInActiveDeployment:
1✔
550
                        d.view.RenderError(w, r, ErrArtifactUsedInActiveDeployment, http.StatusConflict, l)
1✔
551
                }
552
                return
1✔
553
        }
554

555
        d.view.RenderSuccessDelete(w)
1✔
556
}
557

558
func (d *DeploymentsApiHandlers) EditImage(w rest.ResponseWriter, r *rest.Request) {
×
559
        l := requestlog.GetRequestLogger(r)
×
560

×
561
        id := r.PathParam("id")
×
562

×
563
        if !govalidator.IsUUID(id) {
×
564
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
565
                return
×
566
        }
×
567

568
        constructor, err := getImageMetaFromBody(r)
×
569
        if err != nil {
×
570
                d.view.RenderError(
×
571
                        w,
×
572
                        r,
×
573
                        errors.Wrap(err, "Validating request body"),
×
574
                        http.StatusBadRequest,
×
575
                        l,
×
576
                )
×
577
                return
×
578
        }
×
579

580
        found, err := d.app.EditImage(r.Context(), id, constructor)
×
581
        if err != nil {
×
582
                if err == app.ErrModelImageUsedInAnyDeployment {
×
583
                        d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
584
                        return
×
585
                }
×
586
                d.view.RenderInternalError(w, r, err, l)
×
587
                return
×
588
        }
589

590
        if !found {
×
591
                d.view.RenderErrorNotFound(w, r, l)
×
592
                return
×
593
        }
×
594

595
        d.view.RenderSuccessPut(w)
×
596
}
597

598
func getImageMetaFromBody(r *rest.Request) (*model.ImageMeta, error) {
×
599

×
600
        var constructor *model.ImageMeta
×
601

×
602
        if err := r.DecodeJsonPayload(&constructor); err != nil {
×
603
                return nil, err
×
604
        }
×
605

606
        if err := constructor.Validate(); err != nil {
×
607
                return nil, err
×
608
        }
×
609

610
        return constructor, nil
×
611
}
612

613
// NewImage is the Multipart Image/Meta upload handler.
614
// Request should be of type "multipart/form-data". The parts are
615
// key/valyue pairs of metadata information except the last one,
616
// which must contain the artifact file.
617
func (d *DeploymentsApiHandlers) NewImage(w rest.ResponseWriter, r *rest.Request) {
7✔
618
        d.newImageWithContext(r.Context(), w, r)
7✔
619
}
7✔
620

621
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
622
        l := requestlog.GetRequestLogger(r)
7✔
623

7✔
624
        tenantID := r.PathParam("tenant")
7✔
625

7✔
626
        if tenantID == "" {
7✔
627
                rest_utils.RestErrWithLog(
×
628
                        w,
×
629
                        r,
×
630
                        l,
×
631
                        fmt.Errorf("missing tenant id in path"),
×
632
                        http.StatusBadRequest,
×
633
                )
×
634
                return
×
635
        }
×
636

637
        var ctx context.Context
7✔
638
        if tenantID != "default" {
8✔
639
                ident := &identity.Identity{Tenant: tenantID}
1✔
640
                ctx = identity.WithContext(r.Context(), ident)
1✔
641
        } else {
7✔
642
                ctx = r.Context()
6✔
643
        }
6✔
644

645
        d.newImageWithContext(ctx, w, r)
7✔
646
}
647

648
func (d *DeploymentsApiHandlers) newImageWithContext(
649
        ctx context.Context,
650
        w rest.ResponseWriter,
651
        r *rest.Request,
652
) {
13✔
653
        l := requestlog.GetRequestLogger(r)
13✔
654

13✔
655
        formReader, err := r.MultipartReader()
13✔
656
        if err != nil {
17✔
657
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
658
                return
4✔
659
        }
4✔
660

661
        // parse multipart message
662
        multipartUploadMsg, err := d.ParseMultipart(formReader)
9✔
663

9✔
664
        if err != nil {
14✔
665
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
5✔
666
                return
5✔
667
        }
5✔
668

669
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
5✔
670
        if err == nil {
8✔
671
                d.view.RenderSuccessPost(w, r, imgID)
3✔
672
                return
3✔
673
        }
3✔
674
        if cErr, ok := err.(*model.ConflictError); ok {
5✔
675
                d.view.RenderError(w, r, cErr, http.StatusConflict, l)
2✔
676
                return
2✔
677
        }
2✔
678
        cause := errors.Cause(err)
1✔
679
        switch cause {
1✔
680
        default:
×
UNCOV
681
                d.view.RenderInternalError(w, r, err, l)
×
UNCOV
682
                return
×
UNCOV
683
        case app.ErrModelArtifactNotUnique:
×
UNCOV
684
                l.Error(err.Error())
×
UNCOV
685
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
UNCOV
686
                return
×
687
        case app.ErrModelParsingArtifactFailed:
1✔
688
                l.Error(err.Error())
1✔
689
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
690
                return
1✔
691
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
692
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
693
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
694
                l.Error(err.Error())
×
UNCOV
695
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
UNCOV
696
                return
×
697
        }
698
}
699

700
func formatArtifactUploadError(err error) error {
2✔
701
        // remove generic message
2✔
702
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
703

2✔
704
        // handle specific cases
2✔
705

2✔
706
        if strings.Contains(errMsg, "invalid checksum") {
2✔
UNCOV
707
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
UNCOV
708
        }
×
709

710
        if strings.Contains(errMsg, "unsupported version") {
2✔
UNCOV
711
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
UNCOV
712
                        "; supported versions are: 1, 2")
×
UNCOV
713
        }
×
714

715
        return errors.New(errMsg)
2✔
716
}
717

718
// GenerateImage s the multipart Raw Data/Meta upload handler.
719
// Request should be of type "multipart/form-data". The parts are
720
// key/valyue pairs of metadata information except the last one,
721
// which must contain the file containing the raw data to be processed
722
// into an artifact.
723
func (d *DeploymentsApiHandlers) GenerateImage(w rest.ResponseWriter, r *rest.Request) {
13✔
724
        l := requestlog.GetRequestLogger(r)
13✔
725

13✔
726
        formReader, err := r.MultipartReader()
13✔
727
        if err != nil {
15✔
728
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
729
                return
2✔
730
        }
2✔
731

732
        // parse multipart message
733
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
11✔
734
        if err != nil {
15✔
735
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
736
                return
4✔
737
        }
4✔
738

739
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
7✔
740
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
14✔
741
                multipartMsg.Token = tokenFields[1]
7✔
742
        }
7✔
743

744
        imgID, err := d.app.GenerateImage(r.Context(), multipartMsg)
7✔
745
        cause := errors.Cause(err)
7✔
746
        switch cause {
7✔
747
        default:
1✔
748
                d.view.RenderInternalError(w, r, err, l)
1✔
749
        case nil:
3✔
750
                d.view.RenderSuccessPost(w, r, imgID)
3✔
751
        case app.ErrModelArtifactNotUnique:
1✔
752
                l.Error(err.Error())
1✔
753
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
1✔
754
        case app.ErrModelParsingArtifactFailed:
1✔
755
                l.Error(err.Error())
1✔
756
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
757
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
758
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
759
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
760
                l.Error(err.Error())
1✔
761
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
1✔
762
        }
763
}
764

765
// ParseMultipart parses multipart/form-data message.
766
func (d *DeploymentsApiHandlers) ParseMultipart(
767
        r *multipart.Reader,
768
) (*model.MultipartUploadMsg, error) {
9✔
769
        uploadMsg := &model.MultipartUploadMsg{
9✔
770
                MetaConstructor: &model.ImageMeta{},
9✔
771
        }
9✔
772
        var size int64
9✔
773
        // Parse the multipart form sequentially. To remain backward compatible
9✔
774
        // all form names that are not part of the API are ignored.
9✔
775
        for {
38✔
776
                part, err := r.NextPart()
29✔
777
                if err != nil {
31✔
778
                        if err == io.EOF {
4✔
779
                                // The whole message has been consumed without
2✔
780
                                // the "artifact" form part.
2✔
781
                                return nil, ErrArtifactFileMissing
2✔
782
                        }
2✔
UNCOV
783
                        return nil, err
×
784
                }
785
                switch strings.ToLower(part.FormName()) {
27✔
786
                case "description":
7✔
787
                        // Add description to the metadata
7✔
788
                        dscr, err := io.ReadAll(part)
7✔
789
                        if err != nil {
7✔
UNCOV
790
                                return nil, err
×
791
                        }
×
792
                        uploadMsg.MetaConstructor.Description = string(dscr)
7✔
793

794
                case "size":
7✔
795
                        // Add size limit to the metadata
7✔
796
                        sz, err := io.ReadAll(part)
7✔
797
                        if err != nil {
7✔
798
                                return nil, err
×
799
                        }
×
800
                        size, err = strconv.ParseInt(string(sz), 10, 64)
7✔
801
                        if err != nil {
7✔
UNCOV
802
                                return nil, err
×
UNCOV
803
                        }
×
804
                        // Add one since this will impose the upper limit on the
805
                        // artifact size.
806
                        if size > d.config.MaxImageSize {
7✔
807
                                return nil, ErrModelArtifactFileTooLarge
×
UNCOV
808
                        }
×
809

810
                case "artifact_id":
7✔
811
                        // Add artifact id to the metadata (must be a valid UUID).
7✔
812
                        b, err := io.ReadAll(part)
7✔
813
                        if err != nil {
7✔
UNCOV
814
                                return nil, err
×
815
                        }
×
816
                        id := string(b)
7✔
817
                        if !govalidator.IsUUID(id) {
10✔
818
                                return nil, errors.New(
3✔
819
                                        "artifact_id is not a valid UUID",
3✔
820
                                )
3✔
821
                        }
3✔
822
                        uploadMsg.ArtifactID = id
4✔
823

824
                case "artifact":
5✔
825
                        // Assign the form-data payload to the artifact reader
5✔
826
                        // and return. The content is consumed elsewhere.
5✔
827
                        if size > 0 {
10✔
828
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
5✔
829
                        } else {
5✔
UNCOV
830
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
×
UNCOV
831
                                        part,
×
UNCOV
832
                                        d.config.MaxImageSize,
×
UNCOV
833
                                )
×
UNCOV
834
                        }
×
835
                        return uploadMsg, nil
5✔
836

837
                default:
4✔
838
                        // Ignore all non-API sections.
4✔
839
                        continue
4✔
840
                }
841
        }
842
}
843

844
// ParseGenerateImageMultipart parses multipart/form-data message.
845
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
846
        r *multipart.Reader,
847
) (*model.MultipartGenerateImageMsg, error) {
11✔
848
        msg := &model.MultipartGenerateImageMsg{}
11✔
849
        var size int64
11✔
850

11✔
851
ParseLoop:
11✔
852
        for {
65✔
853
                part, err := r.NextPart()
54✔
854
                if err != nil {
56✔
855
                        if err == io.EOF {
4✔
856
                                break
2✔
857
                        }
UNCOV
858
                        return nil, err
×
859
                }
860
                switch strings.ToLower(part.FormName()) {
52✔
861
                case "args":
7✔
862
                        b, err := io.ReadAll(part)
7✔
863
                        if err != nil {
7✔
UNCOV
864
                                return nil, errors.Wrap(err,
×
UNCOV
865
                                        "failed to read form value 'args'",
×
866
                                )
×
UNCOV
867
                        }
×
868
                        msg.Args = string(b)
7✔
869

870
                case "description":
7✔
871
                        b, err := io.ReadAll(part)
7✔
872
                        if err != nil {
7✔
873
                                return nil, errors.Wrap(err,
×
874
                                        "failed to read form value 'description'",
×
875
                                )
×
UNCOV
876
                        }
×
877
                        msg.Description = string(b)
7✔
878

879
                case "device_types_compatible":
9✔
880
                        b, err := io.ReadAll(part)
9✔
881
                        if err != nil {
9✔
882
                                return nil, errors.Wrap(err,
×
883
                                        "failed to read form value 'device_types_compatible'",
×
884
                                )
×
UNCOV
885
                        }
×
886
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
9✔
887

888
                case "file":
9✔
889
                        if size > 0 {
15✔
890
                                msg.FileReader = utils.ReadExactly(part, size)
6✔
891
                        } else {
9✔
892
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxImageSize)
3✔
893
                        }
3✔
894
                        break ParseLoop
9✔
895

896
                case "name":
10✔
897
                        b, err := io.ReadAll(part)
10✔
898
                        if err != nil {
10✔
UNCOV
899
                                return nil, errors.Wrap(err,
×
UNCOV
900
                                        "failed to read form value 'name'",
×
UNCOV
901
                                )
×
UNCOV
902
                        }
×
903
                        msg.Name = string(b)
10✔
904

905
                case "type":
9✔
906
                        b, err := io.ReadAll(part)
9✔
907
                        if err != nil {
9✔
908
                                return nil, errors.Wrap(err,
×
909
                                        "failed to read form value 'type'",
×
910
                                )
×
UNCOV
911
                        }
×
912
                        msg.Type = string(b)
9✔
913

914
                case "size":
6✔
915
                        // Add size limit to the metadata
6✔
916
                        sz, err := io.ReadAll(part)
6✔
917
                        if err != nil {
6✔
918
                                return nil, err
×
919
                        }
×
920
                        size, err = strconv.ParseInt(string(sz), 10, 64)
6✔
921
                        if err != nil {
6✔
UNCOV
922
                                return nil, err
×
UNCOV
923
                        }
×
924
                        // Add one since this will impose the upper limit on the
925
                        // artifact size.
926
                        if size > d.config.MaxImageSize {
6✔
927
                                return nil, ErrModelArtifactFileTooLarge
×
UNCOV
928
                        }
×
929

930
                default:
×
931
                        // Ignore non-API sections.
×
UNCOV
932
                        continue
×
933
                }
934
        }
935

936
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
11✔
937
}
938

939
// deployments
940
func (d *DeploymentsApiHandlers) createDeployment(
941
        w rest.ResponseWriter,
942
        r *rest.Request,
943
        ctx context.Context,
944
        l *log.Logger,
945
        group string,
946
) {
13✔
947
        constructor, err := d.getDeploymentConstructorFromBody(r, group)
13✔
948
        if err != nil {
19✔
949
                d.view.RenderError(
6✔
950
                        w,
6✔
951
                        r,
6✔
952
                        errors.Wrap(err, "Validating request body"),
6✔
953
                        http.StatusBadRequest,
6✔
954
                        l,
6✔
955
                )
6✔
956
                return
6✔
957
        }
6✔
958

959
        id, err := d.app.CreateDeployment(ctx, constructor)
8✔
960
        switch err {
8✔
961
        case nil:
4✔
962
                // in case of deployment to group remove "/group/{name}" from path before creating location
4✔
963
                // haeder
4✔
964
                r.URL.Path = strings.TrimSuffix(r.URL.Path, "/group/"+constructor.Group)
4✔
965
                d.view.RenderSuccessPost(w, r, id)
4✔
966
        case app.ErrNoArtifact:
1✔
967
                d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
1✔
968
        case app.ErrNoDevices:
2✔
969
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
970
        default:
2✔
971
                d.view.RenderInternalError(w, r, err, l)
2✔
972
        }
973
}
974

975
func (d *DeploymentsApiHandlers) PostDeployment(w rest.ResponseWriter, r *rest.Request) {
8✔
976
        ctx := r.Context()
8✔
977
        l := requestlog.GetRequestLogger(r)
8✔
978

8✔
979
        d.createDeployment(w, r, ctx, l, "")
8✔
980
}
8✔
981

982
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
983
        ctx := r.Context()
5✔
984
        l := requestlog.GetRequestLogger(r)
5✔
985

5✔
986
        group := r.PathParam("name")
5✔
987
        if len(group) < 1 {
5✔
UNCOV
988
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
UNCOV
989
        }
×
990
        d.createDeployment(w, r, ctx, l, group)
5✔
991
}
992

993
// parseDeviceConfigurationDeploymentPathParams parses expected params
994
// and check if the params are not empty
995
func parseDeviceConfigurationDeploymentPathParams(r *rest.Request) (string, string, string, error) {
7✔
996
        tenantID := r.PathParam("tenant")
7✔
997
        deviceID := r.PathParam(ParamDeviceID)
7✔
998
        if deviceID == "" {
7✔
UNCOV
999
                return "", "", "", errors.New("device ID missing")
×
UNCOV
1000
        }
×
1001
        deploymentID := r.PathParam(ParamDeploymentID)
7✔
1002
        if deploymentID == "" {
7✔
UNCOV
1003
                return "", "", "", errors.New("deployment ID missing")
×
UNCOV
1004
        }
×
1005
        return tenantID, deviceID, deploymentID, nil
7✔
1006
}
1007

1008
// getConfigurationDeploymentConstructorFromBody extracts configuration
1009
// deployment constructor from the request body and validates it
1010
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
1011
        *model.ConfigurationDeploymentConstructor, error) {
7✔
1012

7✔
1013
        var constructor *model.ConfigurationDeploymentConstructor
7✔
1014

7✔
1015
        if err := r.DecodeJsonPayload(&constructor); err != nil {
8✔
1016
                return nil, err
1✔
1017
        }
1✔
1018

1019
        if err := constructor.Validate(); err != nil {
8✔
1020
                return nil, err
2✔
1021
        }
2✔
1022

1023
        return constructor, nil
5✔
1024
}
1025

1026
// device configuration deployment handler
1027
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1028
        w rest.ResponseWriter,
1029
        r *rest.Request,
1030
) {
7✔
1031
        l := requestlog.GetRequestLogger(r)
7✔
1032

7✔
1033
        // get path params
7✔
1034
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
7✔
1035
        if err != nil {
7✔
UNCOV
1036
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
UNCOV
1037
                return
×
UNCOV
1038
        }
×
1039

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

7✔
1043
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
7✔
1044
        if err != nil {
10✔
1045
                d.view.RenderError(
3✔
1046
                        w,
3✔
1047
                        r,
3✔
1048
                        errors.Wrap(err, "Validating request body"),
3✔
1049
                        http.StatusBadRequest,
3✔
1050
                        l,
3✔
1051
                )
3✔
1052
                return
3✔
1053
        }
3✔
1054

1055
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
5✔
1056
        switch err {
5✔
1057
        default:
1✔
1058
                d.view.RenderInternalError(w, r, err, l)
1✔
1059
        case nil:
3✔
1060
                r.URL.Path = "./deployments"
3✔
1061
                d.view.RenderSuccessPost(w, r, id)
3✔
1062
        case app.ErrDuplicateDeployment:
2✔
1063
                d.view.RenderError(w, r, err, http.StatusConflict, l)
2✔
1064
        case app.ErrInvalidDeploymentID:
1✔
1065
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1066
        }
1067
}
1068

1069
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1070
        r *rest.Request,
1071
        group string,
1072
) (*model.DeploymentConstructor, error) {
13✔
1073
        var constructor *model.DeploymentConstructor
13✔
1074
        if err := r.DecodeJsonPayload(&constructor); err != nil {
16✔
1075
                return nil, err
3✔
1076
        }
3✔
1077

1078
        constructor.Group = group
11✔
1079

11✔
1080
        if err := constructor.ValidateNew(); err != nil {
15✔
1081
                return nil, err
4✔
1082
        }
4✔
1083

1084
        return constructor, nil
8✔
1085
}
1086

1087
func (d *DeploymentsApiHandlers) GetDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1088
        ctx := r.Context()
1✔
1089
        l := requestlog.GetRequestLogger(r)
1✔
1090

1✔
1091
        id := r.PathParam("id")
1✔
1092

1✔
1093
        if !govalidator.IsUUID(id) {
2✔
1094
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
1095
                return
1✔
1096
        }
1✔
1097

1098
        deployment, err := d.app.GetDeployment(ctx, id)
1✔
1099
        if err != nil {
1✔
UNCOV
1100
                d.view.RenderInternalError(w, r, err, l)
×
UNCOV
1101
                return
×
UNCOV
1102
        }
×
1103

1104
        if deployment == nil {
1✔
UNCOV
1105
                d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
1106
                return
×
UNCOV
1107
        }
×
1108

1109
        d.view.RenderSuccessGet(w, deployment)
1✔
1110
}
1111

1112
func (d *DeploymentsApiHandlers) GetDeploymentStats(w rest.ResponseWriter, r *rest.Request) {
1✔
1113
        ctx := r.Context()
1✔
1114
        l := requestlog.GetRequestLogger(r)
1✔
1115

1✔
1116
        id := r.PathParam("id")
1✔
1117

1✔
1118
        if !govalidator.IsUUID(id) {
1✔
UNCOV
1119
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
UNCOV
1120
                return
×
UNCOV
1121
        }
×
1122

1123
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1124
        if err != nil {
1✔
UNCOV
1125
                d.view.RenderInternalError(w, r, err, l)
×
UNCOV
1126
                return
×
1127
        }
×
1128

1129
        if stats == nil {
1✔
UNCOV
1130
                d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
1131
                return
×
UNCOV
1132
        }
×
1133

1134
        d.view.RenderSuccessGet(w, stats)
1✔
1135
}
1136

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

4✔
1139
        ctx := r.Context()
4✔
1140
        l := requestlog.GetRequestLogger(r)
4✔
1141

4✔
1142
        ids := model.DeploymentIDs{}
4✔
1143
        if err := r.DecodeJsonPayload(&ids); err != nil {
4✔
UNCOV
1144
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1145
                return
×
UNCOV
1146
        }
×
1147

1148
        if len(ids.IDs) == 0 {
4✔
UNCOV
1149
                w.WriteHeader(http.StatusOK)
×
UNCOV
1150
                _ = w.WriteJson(struct{}{})
×
UNCOV
1151
                return
×
1152
        }
×
1153

1154
        if err := ids.Validate(); err != nil {
5✔
1155
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1156
                return
1✔
1157
        }
1✔
1158

1159
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
3✔
1160
        if err != nil {
5✔
1161
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
3✔
1162
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
1✔
1163
                        return
1✔
1164
                }
1✔
1165
                d.view.RenderInternalError(w, r, err, l)
1✔
1166
                return
1✔
1167
        }
1168

1169
        w.WriteHeader(http.StatusOK)
1✔
1170

1✔
1171
        _ = w.WriteJson(stats)
1✔
1172
}
1173

UNCOV
1174
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1175
        ctx := r.Context()
×
UNCOV
1176
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1177

×
UNCOV
1178
        id := r.PathParam("id")
×
UNCOV
1179

×
UNCOV
1180
        if !govalidator.IsUUID(id) {
×
UNCOV
1181
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1182
                return
×
1183
        }
×
1184

1185
        deployment, err := d.app.GetDeployment(ctx, id)
×
1186
        if err != nil {
×
1187
                d.view.RenderInternalError(w, r, err, l)
×
1188
                return
×
1189
        }
×
1190

1191
        if deployment == nil {
×
UNCOV
1192
                d.view.RenderErrorNotFound(w, r, l)
×
1193
                return
×
1194
        }
×
1195

1196
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1197
}
1198

1199
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1200
        ctx := r.Context()
1✔
1201
        l := requestlog.GetRequestLogger(r)
1✔
1202

1✔
1203
        id := r.PathParam("id")
1✔
1204

1✔
1205
        if !govalidator.IsUUID(id) {
1✔
UNCOV
1206
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
UNCOV
1207
                return
×
UNCOV
1208
        }
×
1209

1210
        // receive request body
1211
        var status struct {
1✔
1212
                Status model.DeviceDeploymentStatus
1✔
1213
        }
1✔
1214

1✔
1215
        err := r.DecodeJsonPayload(&status)
1✔
1216
        if err != nil {
1✔
UNCOV
1217
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1218
                return
×
UNCOV
1219
        }
×
1220
        // "aborted" is the only supported status
1221
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
UNCOV
1222
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
UNCOV
1223
        }
×
1224

1225
        l.Infof("Abort deployment: %s", id)
1✔
1226

1✔
1227
        // Check if deployment is finished
1✔
1228
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1229
        if err != nil {
1✔
1230
                d.view.RenderInternalError(w, r, err, l)
×
1231
                return
×
UNCOV
1232
        }
×
1233
        if isDeploymentFinished {
2✔
1234
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
1✔
1235
                return
1✔
1236
        }
1✔
1237

1238
        // Abort deployments for devices and update deployment stats
1239
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1240
                d.view.RenderInternalError(w, r, err, l)
×
UNCOV
1241
        }
×
1242

1243
        d.view.RenderEmptySuccessResponse(w)
1✔
1244
}
1245

1246
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
12✔
1247
        var (
12✔
1248
                installed *model.InstalledDeviceDeployment
12✔
1249
                ctx       = r.Context()
12✔
1250
                l         = requestlog.GetRequestLogger(r)
12✔
1251
                idata     = identity.FromContext(ctx)
12✔
1252
        )
12✔
1253
        if idata == nil {
14✔
1254
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
2✔
1255
                return
2✔
1256
        }
2✔
1257

1258
        q := r.URL.Query()
11✔
1259
        defer func() {
22✔
1260
                var reEncode bool = false
11✔
1261
                if name := q.Get(ParamArtifactName); name != "" {
20✔
1262
                        q.Set(ParamArtifactName, Redacted)
9✔
1263
                        reEncode = true
9✔
1264
                }
9✔
1265
                if typ := q.Get(ParamDeviceType); typ != "" {
20✔
1266
                        q.Set(ParamDeviceType, Redacted)
9✔
1267
                        reEncode = true
9✔
1268
                }
9✔
1269
                if reEncode {
20✔
1270
                        r.URL.RawQuery = q.Encode()
9✔
1271
                }
9✔
1272
        }()
1273
        if strings.EqualFold(r.Method, http.MethodPost) {
13✔
1274
                // POST
2✔
1275
                installed = new(model.InstalledDeviceDeployment)
2✔
1276
                if err := r.DecodeJsonPayload(&installed); err != nil {
3✔
1277
                        d.view.RenderError(w, r,
1✔
1278
                                errors.Wrap(err, "invalid schema"),
1✔
1279
                                http.StatusBadRequest, l)
1✔
1280
                        return
1✔
1281
                }
1✔
1282
        } else {
9✔
1283
                // GET or HEAD
9✔
1284
                installed = &model.InstalledDeviceDeployment{
9✔
1285
                        ArtifactName: q.Get(ParamArtifactName),
9✔
1286
                        DeviceType:   q.Get(ParamDeviceType),
9✔
1287
                }
9✔
1288
        }
9✔
1289

1290
        if err := installed.Validate(); err != nil {
11✔
1291
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1292
                return
1✔
1293
        }
1✔
1294

1295
        request := &model.DeploymentNextRequest{
9✔
1296
                DeviceProvides: installed,
9✔
1297
        }
9✔
1298

9✔
1299
        d.getDeploymentForDevice(w, r, idata, request)
9✔
1300
}
1301

1302
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1303
        w rest.ResponseWriter,
1304
        r *rest.Request,
1305
        idata *identity.Identity,
1306
        request *model.DeploymentNextRequest,
1307
) {
9✔
1308
        ctx := r.Context()
9✔
1309
        l := requestlog.GetRequestLogger(r)
9✔
1310

9✔
1311
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
9✔
1312
        if err != nil {
11✔
1313
                if err == app.ErrConflictingRequestData {
3✔
1314
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1315
                } else {
2✔
1316
                        d.view.RenderInternalError(w, r, err, l)
1✔
1317
                }
1✔
1318
                return
2✔
1319
        }
1320

1321
        if deployment == nil {
10✔
1322
                d.view.RenderNoUpdateForDevice(w)
2✔
1323
                return
2✔
1324
        } else if deployment.Type == model.DeploymentTypeConfiguration {
14✔
1325
                // Generate pre-signed URL
5✔
1326
                var hostName string = d.config.PresignHostname
5✔
1327
                if hostName == "" {
7✔
1328
                        if hostName = r.Header.Get(hdrForwardedHost); hostName == "" {
3✔
1329
                                d.view.RenderInternalError(w, r,
1✔
1330
                                        errors.New("presign.hostname not configured; "+
1✔
1331
                                                "unable to generate download link "+
1✔
1332
                                                " for configuration deployment"), l)
1✔
1333
                                return
1✔
1334
                        }
1✔
1335
                }
1336
                req, _ := http.NewRequest(
4✔
1337
                        http.MethodGet,
4✔
1338
                        FMTConfigURL(
4✔
1339
                                d.config.PresignScheme, hostName,
4✔
1340
                                deployment.ID, request.DeviceProvides.DeviceType,
4✔
1341
                                idata.Subject,
4✔
1342
                        ),
4✔
1343
                        nil,
4✔
1344
                )
4✔
1345
                if idata.Tenant != "" {
7✔
1346
                        q := req.URL.Query()
3✔
1347
                        q.Set(model.ParamTenantID, idata.Tenant)
3✔
1348
                        req.URL.RawQuery = q.Encode()
3✔
1349
                }
3✔
1350
                sig := model.NewRequestSignature(req, d.config.PresignSecret)
4✔
1351
                expireTS := time.Now().Add(d.config.PresignExpire)
4✔
1352
                sig.SetExpire(expireTS)
4✔
1353
                deployment.Artifact.Source = model.Link{
4✔
1354
                        Uri:    sig.PresignURL(),
4✔
1355
                        Expire: expireTS,
4✔
1356
                }
4✔
1357
        }
1358

1359
        d.view.RenderSuccessGet(w, deployment)
6✔
1360
}
1361

1362
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1363
        w rest.ResponseWriter,
1364
        r *rest.Request,
1365
) {
1✔
1366
        ctx := r.Context()
1✔
1367
        l := requestlog.GetRequestLogger(r)
1✔
1368

1✔
1369
        did := r.PathParam("id")
1✔
1370

1✔
1371
        idata := identity.FromContext(ctx)
1✔
1372
        if idata == nil {
1✔
UNCOV
1373
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
UNCOV
1374
                return
×
UNCOV
1375
        }
×
1376

1377
        // receive request body
1378
        var report model.StatusReport
1✔
1379

1✔
1380
        err := r.DecodeJsonPayload(&report)
1✔
1381
        if err != nil {
1✔
1382
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1383
                return
×
UNCOV
1384
        }
×
1385

1386
        l.Infof("status: %+v", report)
1✔
1387
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
1✔
1388
                idata.Subject, model.DeviceDeploymentState{
1✔
1389
                        Status:   report.Status,
1✔
1390
                        SubState: report.SubState,
1✔
1391
                }); err != nil {
1✔
1392

×
UNCOV
1393
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
UNCOV
1394
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
UNCOV
1395
                } else if err == app.ErrStorageNotFound {
×
UNCOV
1396
                        d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
1397
                } else {
×
UNCOV
1398
                        d.view.RenderInternalError(w, r, err, l)
×
UNCOV
1399
                }
×
1400
                return
×
1401
        }
1402

1403
        d.view.RenderEmptySuccessResponse(w)
1✔
1404
}
1405

1406
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1407
        w rest.ResponseWriter,
1408
        r *rest.Request,
1409
) {
1✔
1410
        ctx := r.Context()
1✔
1411
        l := requestlog.GetRequestLogger(r)
1✔
1412

1✔
1413
        did := r.PathParam("id")
1✔
1414

1✔
1415
        if !govalidator.IsUUID(did) {
1✔
UNCOV
1416
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
UNCOV
1417
                return
×
UNCOV
1418
        }
×
1419

1420
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
1✔
1421
        if err != nil {
1✔
UNCOV
1422
                switch err {
×
UNCOV
1423
                case app.ErrModelDeploymentNotFound:
×
1424
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1425
                        return
×
1426
                default:
×
UNCOV
1427
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
UNCOV
1428
                        return
×
1429
                }
1430
        }
1431

1432
        d.view.RenderSuccessGet(w, statuses)
1✔
1433
}
1434

1435
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1436
        w rest.ResponseWriter,
1437
        r *rest.Request,
1438
) {
1✔
1439
        ctx := r.Context()
1✔
1440
        l := requestlog.GetRequestLogger(r)
1✔
1441

1✔
1442
        did := r.PathParam("id")
1✔
1443

1✔
1444
        if !govalidator.IsUUID(did) {
1✔
UNCOV
1445
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
UNCOV
1446
                return
×
UNCOV
1447
        }
×
1448

1449
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1450
        if err != nil {
1✔
UNCOV
1451
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1452
                return
×
1453
        }
×
1454

1455
        lq := store.ListQuery{
1✔
1456
                Skip:         int((page - 1) * perPage),
1✔
1457
                Limit:        int(perPage),
1✔
1458
                DeploymentID: did,
1✔
1459
        }
1✔
1460
        if status := r.URL.Query().Get("status"); status != "" {
1✔
1461
                lq.Status = &status
×
UNCOV
1462
        }
×
1463
        if err = lq.Validate(); err != nil {
1✔
UNCOV
1464
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1465
                return
×
UNCOV
1466
        }
×
1467

1468
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1469
        if err != nil {
1✔
1470
                switch err {
×
UNCOV
1471
                case app.ErrModelDeploymentNotFound:
×
1472
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1473
                        return
×
1474
                default:
×
UNCOV
1475
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
UNCOV
1476
                        return
×
1477
                }
1478
        }
1479

1480
        hasNext := totalCount > int(page*perPage)
1✔
1481
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1482
        for _, l := range links {
2✔
1483
                w.Header().Add("Link", l)
1✔
1484
        }
1✔
1485
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1486
        d.view.RenderSuccessGet(w, statuses)
1✔
1487
}
1488

1489
func ParseLookupQuery(vals url.Values) (model.Query, error) {
9✔
1490
        query := model.Query{}
9✔
1491

9✔
1492
        search := vals.Get("search")
9✔
1493
        if search != "" {
9✔
UNCOV
1494
                query.SearchText = search
×
UNCOV
1495
        }
×
1496

1497
        createdBefore := vals.Get("created_before")
9✔
1498
        if createdBefore != "" {
10✔
1499
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
2✔
1500
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1501
                } else {
1✔
1502
                        query.CreatedBefore = &createdBeforeTime
×
1503
                }
×
1504
        }
1505

1506
        createdAfter := vals.Get("created_after")
8✔
1507
        if createdAfter != "" {
8✔
UNCOV
1508
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
UNCOV
1509
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1510
                } else {
×
1511
                        query.CreatedAfter = &createdAfterTime
×
UNCOV
1512
                }
×
1513
        }
1514

1515
        switch strings.ToLower(vals.Get("sort")) {
8✔
1516
        case model.SortDirectionAscending:
1✔
1517
                query.Sort = model.SortDirectionAscending
1✔
1518
        case "", model.SortDirectionDescending:
7✔
1519
                query.Sort = model.SortDirectionDescending
7✔
1520
        default:
×
UNCOV
1521
                return query, ErrInvalidSortDirection
×
1522
        }
1523

1524
        status := vals.Get("status")
8✔
1525
        switch status {
8✔
UNCOV
1526
        case "inprogress":
×
UNCOV
1527
                query.Status = model.StatusQueryInProgress
×
1528
        case "finished":
×
1529
                query.Status = model.StatusQueryFinished
×
UNCOV
1530
        case "pending":
×
UNCOV
1531
                query.Status = model.StatusQueryPending
×
UNCOV
1532
        case "aborted":
×
UNCOV
1533
                query.Status = model.StatusQueryAborted
×
1534
        case "":
8✔
1535
                query.Status = model.StatusQueryAny
8✔
1536
        default:
×
1537
                return query, errors.Errorf("unknown status %s", status)
×
1538

1539
        }
1540

1541
        dType := vals.Get("type")
8✔
1542
        if dType == "" {
16✔
1543
                return query, nil
8✔
1544
        }
8✔
1545
        deploymentType := model.DeploymentType(dType)
×
UNCOV
1546
        if deploymentType == model.DeploymentTypeSoftware ||
×
UNCOV
1547
                deploymentType == model.DeploymentTypeConfiguration {
×
UNCOV
1548
                query.Type = deploymentType
×
UNCOV
1549
        } else {
×
UNCOV
1550
                return query, errors.Errorf("unknown deployment type %s", dType)
×
UNCOV
1551
        }
×
1552

1553
        return query, nil
×
1554
}
1555

1556
func parseEpochToTimestamp(epoch string) (time.Time, error) {
1✔
1557
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
2✔
1558
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
1✔
1559
        } else {
1✔
UNCOV
1560
                return time.Unix(epochInt64, 0).UTC(), nil
×
1561
        }
×
1562
}
1563

1564
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
9✔
1565
        ctx := r.Context()
9✔
1566
        l := requestlog.GetRequestLogger(r)
9✔
1567
        q := r.URL.Query()
9✔
1568
        defer func() {
18✔
1569
                if search := q.Get("search"); search != "" {
9✔
UNCOV
1570
                        q.Set("search", Redacted)
×
UNCOV
1571
                        r.URL.RawQuery = q.Encode()
×
UNCOV
1572
                }
×
1573
        }()
1574

1575
        query, err := ParseLookupQuery(q)
9✔
1576
        if err != nil {
10✔
1577
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1578
                return
1✔
1579
        }
1✔
1580

1581
        page, perPage, err := rest_utils.ParsePagination(r)
8✔
1582
        if err != nil {
9✔
1583
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1584
                return
1✔
1585
        }
1✔
1586
        query.Skip = int((page - 1) * perPage)
7✔
1587
        query.Limit = int(perPage + 1)
7✔
1588

7✔
1589
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
7✔
1590
        if err != nil {
8✔
1591
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1592
                return
1✔
1593
        }
1✔
1594
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
6✔
1595

6✔
1596
        len := len(deps)
6✔
1597
        hasNext := false
6✔
1598
        if uint64(len) > perPage {
6✔
UNCOV
1599
                hasNext = true
×
UNCOV
1600
                len = int(perPage)
×
UNCOV
1601
        }
×
1602

1603
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
6✔
1604
        for _, l := range links {
13✔
1605
                w.Header().Add("Link", l)
7✔
1606
        }
7✔
1607

1608
        d.view.RenderSuccessGet(w, deps[:len])
6✔
1609
}
1610

1611
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1612
        ctx := r.Context()
1✔
1613
        l := requestlog.GetRequestLogger(r)
1✔
1614

1✔
1615
        did := r.PathParam("id")
1✔
1616

1✔
1617
        idata := identity.FromContext(ctx)
1✔
1618
        if idata == nil {
1✔
UNCOV
1619
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
UNCOV
1620
                return
×
UNCOV
1621
        }
×
1622

1623
        // reuse DeploymentLog, device and deployment IDs are ignored when
1624
        // (un-)marshaling DeploymentLog to/from JSON
1625
        var log model.DeploymentLog
1✔
1626

1✔
1627
        err := r.DecodeJsonPayload(&log)
1✔
1628
        if err != nil {
1✔
1629
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1630
                return
×
UNCOV
1631
        }
×
1632

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

×
UNCOV
1636
                if err == app.ErrModelDeploymentNotFound {
×
1637
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1638
                } else {
×
1639
                        d.view.RenderInternalError(w, r, err, l)
×
UNCOV
1640
                }
×
UNCOV
1641
                return
×
1642
        }
1643

1644
        d.view.RenderEmptySuccessResponse(w)
1✔
1645
}
1646

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

1✔
1651
        did := r.PathParam("id")
1✔
1652
        devid := r.PathParam("devid")
1✔
1653

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

1✔
1656
        if err != nil {
1✔
UNCOV
1657
                d.view.RenderInternalError(w, r, err, l)
×
UNCOV
1658
                return
×
UNCOV
1659
        }
×
1660

1661
        if depl == nil {
1✔
UNCOV
1662
                d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
1663
                return
×
UNCOV
1664
        }
×
1665

1666
        d.view.RenderDeploymentLog(w, *depl)
1✔
1667
}
1668

1669
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
3✔
1670
        ctx := r.Context()
3✔
1671
        l := requestlog.GetRequestLogger(r)
3✔
1672

3✔
1673
        id := r.PathParam("id")
3✔
1674
        err := d.app.AbortDeviceDeployments(ctx, id)
3✔
1675

3✔
1676
        switch err {
3✔
1677
        case nil, app.ErrStorageNotFound:
2✔
1678
                d.view.RenderEmptySuccessResponse(w)
2✔
1679
        default:
1✔
1680
                d.view.RenderInternalError(w, r, err, l)
1✔
1681
        }
1682
}
1683

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

3✔
1689
        id := r.PathParam("id")
3✔
1690
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
3✔
1691

3✔
1692
        switch err {
3✔
1693
        case nil, app.ErrStorageNotFound:
2✔
1694
                d.view.RenderEmptySuccessResponse(w)
2✔
1695
        default:
1✔
1696
                d.view.RenderInternalError(w, r, err, l)
1✔
1697
        }
1698
}
1699

1700
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
9✔
1701
        ctx := r.Context()
9✔
1702
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1703
}
9✔
1704

1705
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1706
        r *rest.Request) {
9✔
1707
        ctx := r.Context()
9✔
1708
        tenantID := r.PathParam("tenant")
9✔
1709
        if tenantID != "" {
18✔
1710
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1711
                        Tenant:   tenantID,
9✔
1712
                        IsDevice: true,
9✔
1713
                })
9✔
1714
        }
9✔
1715
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1716
}
1717

1718
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(w rest.ResponseWriter,
1719
        r *rest.Request) {
9✔
1720
        ctx := r.Context()
9✔
1721
        tenantID := r.PathParam("tenant")
9✔
1722
        if tenantID != "" {
18✔
1723
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1724
                        Tenant:   tenantID,
9✔
1725
                        IsDevice: true,
9✔
1726
                })
9✔
1727
        }
9✔
1728
        d.listDeviceDeployments(ctx, w, r, false)
9✔
1729
}
1730

1731
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1732
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
27✔
1733
        l := requestlog.GetRequestLogger(r)
27✔
1734

27✔
1735
        did := ""
27✔
1736
        var IDs []string
27✔
1737
        if byDeviceID {
45✔
1738
                did = r.PathParam("id")
18✔
1739
        } else {
27✔
1740
                values := r.URL.Query()
9✔
1741
                if values.Has("id") && len(values["id"]) > 0 {
17✔
1742
                        IDs = values["id"]
8✔
1743
                } else {
9✔
1744
                        d.view.RenderError(w, r, ErrEmptyID, http.StatusBadRequest, l)
1✔
1745
                        return
1✔
1746
                }
1✔
1747
        }
1748

1749
        page, perPage, err := rest_utils.ParsePagination(r)
26✔
1750
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
29✔
1751
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
3✔
1752
        }
3✔
1753
        if err != nil {
32✔
1754
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
6✔
1755
                return
6✔
1756
        }
6✔
1757

1758
        lq := store.ListQueryDeviceDeployments{
20✔
1759
                Skip:     int((page - 1) * perPage),
20✔
1760
                Limit:    int(perPage),
20✔
1761
                DeviceID: did,
20✔
1762
                IDs:      IDs,
20✔
1763
        }
20✔
1764
        if status := r.URL.Query().Get("status"); status != "" {
26✔
1765
                lq.Status = &status
6✔
1766
        }
6✔
1767
        if err = lq.Validate(); err != nil {
23✔
1768
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
3✔
1769
                return
3✔
1770
        }
3✔
1771

1772
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
17✔
1773
        if err != nil {
20✔
1774
                d.view.RenderInternalError(w, r, err, l)
3✔
1775
                return
3✔
1776
        }
3✔
1777
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
14✔
1778

14✔
1779
        hasNext := totalCount > lq.Skip+len(deps)
14✔
1780
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
14✔
1781
        for _, l := range links {
28✔
1782
                w.Header().Add("Link", l)
14✔
1783
        }
14✔
1784

1785
        d.view.RenderSuccessGet(w, deps)
14✔
1786
}
1787

1788
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
UNCOV
1789
        r *rest.Request) {
×
UNCOV
1790
        ctx := r.Context()
×
UNCOV
1791
        tenantID := r.PathParam("tenantID")
×
UNCOV
1792
        if tenantID != "" {
×
UNCOV
1793
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
UNCOV
1794
                        Tenant:   tenantID,
×
UNCOV
1795
                        IsDevice: true,
×
UNCOV
1796
                })
×
1797
        }
×
1798

1799
        l := requestlog.GetRequestLogger(r)
×
1800

×
1801
        id := r.PathParam("id")
×
1802

×
1803
        // Decommission deployments for devices and update deployment stats
×
1804
        err := d.app.DecommissionDevice(ctx, id)
×
1805

×
UNCOV
1806
        switch err {
×
1807
        case nil, app.ErrStorageNotFound:
×
1808
                d.view.RenderEmptySuccessResponse(w)
×
1809
        default:
×
1810
                d.view.RenderInternalError(w, r, err, l)
×
1811

1812
        }
1813
}
1814

1815
// tenants
1816

1817
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1818
        ctx := r.Context()
1✔
1819
        l := requestlog.GetRequestLogger(r)
1✔
1820

1✔
1821
        defer r.Body.Close()
1✔
1822

1✔
1823
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1824
        if err != nil {
2✔
1825
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1826
                return
1✔
1827
        }
1✔
1828

1829
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1830
        if err != nil {
1✔
UNCOV
1831
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
UNCOV
1832
                return
×
UNCOV
1833
        }
×
1834

1835
        w.WriteHeader(http.StatusCreated)
1✔
1836
}
1837

1838
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1839
        w rest.ResponseWriter,
1840
        r *rest.Request,
1841
) {
6✔
1842
        tenantID := r.PathParam("tenant")
6✔
1843
        if tenantID == "" {
7✔
1844
                l := requestlog.GetRequestLogger(r)
1✔
1845
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1846
                return
1✔
1847
        }
1✔
1848

1849
        r.Request = r.WithContext(identity.WithContext(
5✔
1850
                r.Context(),
5✔
1851
                &identity.Identity{Tenant: tenantID},
5✔
1852
        ))
5✔
1853
        d.LookupDeployment(w, r)
5✔
1854
}
1855

1856
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1857
        w rest.ResponseWriter,
1858
        r *rest.Request,
1859
) {
5✔
1860
        l := requestlog.GetRequestLogger(r)
5✔
1861

5✔
1862
        tenantID := r.PathParam("tenant")
5✔
1863

5✔
1864
        ctx := identity.WithContext(
5✔
1865
                r.Context(),
5✔
1866
                &identity.Identity{Tenant: tenantID},
5✔
1867
        )
5✔
1868

5✔
1869
        settings, err := d.app.GetStorageSettings(ctx)
5✔
1870
        if err != nil {
7✔
1871
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1872
                return
2✔
1873
        }
2✔
1874

1875
        d.view.RenderSuccessGet(w, settings)
3✔
1876
}
1877

1878
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1879
        w rest.ResponseWriter,
1880
        r *rest.Request,
1881
) {
10✔
1882
        l := requestlog.GetRequestLogger(r)
10✔
1883

10✔
1884
        defer r.Body.Close()
10✔
1885

10✔
1886
        tenantID := r.PathParam("tenant")
10✔
1887

10✔
1888
        ctx := identity.WithContext(
10✔
1889
                r.Context(),
10✔
1890
                &identity.Identity{Tenant: tenantID},
10✔
1891
        )
10✔
1892

10✔
1893
        settings, err := model.ParseStorageSettingsRequest(r.Body)
10✔
1894
        if err != nil {
13✔
1895
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
1896
                return
3✔
1897
        }
3✔
1898

1899
        err = d.app.SetStorageSettings(ctx, settings)
8✔
1900
        if err != nil {
10✔
1901
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1902
                return
2✔
1903
        }
2✔
1904

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