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

mendersoftware / deployments / 731956009

pending completion
731956009

Pull #809

gitlab-ci

Fabio Tranchitella
feat: add support to filter device deployments by status `finished`
Pull Request #809: MEN-5911: feat: internal end-point to retrieve the device deployments history

95 of 131 new or added lines in 7 files covered. (72.52%)

1 existing line in 1 file now uncovered.

6112 of 7840 relevant lines covered (77.96%)

77.37 hits per line

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

75.96
/api/http/api_deployments.go
1
// Copyright 2022 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/requestlog"
36
        "github.com/mendersoftware/go-lib-micro/rest_utils"
37

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

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

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

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

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

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

83
const Redacted = "REDACTED"
84

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

91
const (
92
        defaultTimeout = time.Second * 10
93
)
94

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

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

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

123
type Config struct {
124
        // URL signing parameters:
125

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

138
func NewConfig() *Config {
307✔
139
        return &Config{
307✔
140
                PresignExpire: DefaultDownloadLinkExpire,
307✔
141
                PresignScheme: "https",
307✔
142
                MaxImageSize:  DefaultMaxImageSize,
307✔
143
        }
307✔
144
}
307✔
145

146
func (conf *Config) SetPresignSecret(key []byte) *Config {
39✔
147
        conf.PresignSecret = key
39✔
148
        return conf
39✔
149
}
39✔
150

151
func (conf *Config) SetPresignExpire(duration time.Duration) *Config {
27✔
152
        conf.PresignExpire = duration
27✔
153
        return conf
27✔
154
}
27✔
155

156
func (conf *Config) SetPresignHostname(hostname string) *Config {
23✔
157
        conf.PresignHostname = hostname
23✔
158
        return conf
23✔
159
}
23✔
160

161
func (conf *Config) SetPresignScheme(scheme string) *Config {
27✔
162
        conf.PresignScheme = scheme
27✔
163
        return conf
27✔
164
}
27✔
165

166
func (conf *Config) SetMaxImageSize(size int64) *Config {
1✔
167
        conf.MaxImageSize = size
1✔
168
        return conf
1✔
169
}
1✔
170

171
type DeploymentsApiHandlers struct {
172
        view   RESTView
173
        store  store.DataStore
174
        app    app.App
175
        config Config
176
}
177

178
func NewDeploymentsApiHandlers(
179
        store store.DataStore,
180
        view RESTView,
181
        app app.App,
182
        config ...*Config,
183
) *DeploymentsApiHandlers {
269✔
184
        conf := NewConfig()
269✔
185
        for _, c := range config {
310✔
186
                if c == nil {
43✔
187
                        continue
2✔
188
                }
189
                if c.PresignSecret != nil {
78✔
190
                        conf.PresignSecret = c.PresignSecret
39✔
191
                }
39✔
192
                if c.PresignExpire != 0 {
78✔
193
                        conf.PresignExpire = c.PresignExpire
39✔
194
                }
39✔
195
                if c.PresignHostname != "" {
62✔
196
                        conf.PresignHostname = c.PresignHostname
23✔
197
                }
23✔
198
                if c.PresignScheme != "" {
78✔
199
                        conf.PresignScheme = c.PresignScheme
39✔
200
                }
39✔
201
                if c.MaxImageSize > 0 {
78✔
202
                        conf.MaxImageSize = c.MaxImageSize
39✔
203
                }
39✔
204
        }
205
        return &DeploymentsApiHandlers{
269✔
206
                store:  store,
269✔
207
                view:   view,
269✔
208
                app:    app,
269✔
209
                config: *conf,
269✔
210
        }
269✔
211
}
212

213
func (u *DeploymentsApiHandlers) AliveHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
214
        w.WriteHeader(http.StatusNoContent)
2✔
215
}
2✔
216

217
func (u *DeploymentsApiHandlers) HealthHandler(w rest.ResponseWriter, r *rest.Request) {
4✔
218
        ctx := r.Context()
4✔
219
        l := log.FromContext(ctx)
4✔
220
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
4✔
221
        defer cancel()
4✔
222

4✔
223
        err := u.app.HealthCheck(ctx)
4✔
224
        if err != nil {
6✔
225
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
2✔
226
                return
2✔
227
        }
2✔
228
        w.WriteHeader(http.StatusNoContent)
2✔
229
}
230

231
func getReleaseOrImageFilter(r *rest.Request, paginated bool) *model.ReleaseOrImageFilter {
49✔
232

49✔
233
        q := r.URL.Query()
49✔
234

49✔
235
        filter := &model.ReleaseOrImageFilter{
49✔
236
                Name:        q.Get(ParamName),
49✔
237
                Description: q.Get(ParamDescription),
49✔
238
                DeviceType:  q.Get(ParamDeviceType),
49✔
239
        }
49✔
240

49✔
241
        if paginated {
73✔
242
                filter.Sort = q.Get(ParamSort)
24✔
243
                if page := q.Get(ParamPage); page != "" {
26✔
244
                        if i, err := strconv.Atoi(page); err == nil {
4✔
245
                                filter.Page = i
2✔
246
                        }
2✔
247
                }
248
                if perPage := q.Get(ParamPerPage); perPage != "" {
28✔
249
                        if i, err := strconv.Atoi(perPage); err == nil {
8✔
250
                                filter.PerPage = i
4✔
251
                        }
4✔
252
                }
253
                if filter.Page <= 0 {
46✔
254
                        filter.Page = 1
22✔
255
                }
22✔
256
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
46✔
257
                        filter.PerPage = DefaultPerPage
22✔
258
                }
22✔
259
        }
260

261
        return filter
49✔
262
}
263

264
func redactReleaseName(r *rest.Request) {
33✔
265
        q := r.URL.Query()
33✔
266
        if q.Get(ParamName) != "" {
42✔
267
                q.Set(ParamName, Redacted)
9✔
268
                r.URL.RawQuery = q.Encode()
9✔
269
        }
9✔
270
}
271

272
func (d *DeploymentsApiHandlers) GetReleases(w rest.ResponseWriter, r *rest.Request) {
9✔
273
        l := requestlog.GetRequestLogger(r)
9✔
274

9✔
275
        defer redactReleaseName(r)
9✔
276
        filter := getReleaseOrImageFilter(r, false)
9✔
277
        releases, _, err := d.store.GetReleases(r.Context(), filter)
9✔
278
        if err != nil {
11✔
279
                d.view.RenderInternalError(w, r, err, l)
2✔
280
                return
2✔
281
        }
2✔
282

283
        d.view.RenderSuccessGet(w, releases)
7✔
284
}
285

286
func (d *DeploymentsApiHandlers) ListReleases(w rest.ResponseWriter, r *rest.Request) {
8✔
287
        l := requestlog.GetRequestLogger(r)
8✔
288

8✔
289
        defer redactReleaseName(r)
8✔
290
        filter := getReleaseOrImageFilter(r, true)
8✔
291
        releases, totalCount, err := d.store.GetReleases(r.Context(), filter)
8✔
292
        if err != nil {
10✔
293
                d.view.RenderInternalError(w, r, err, l)
2✔
294
                return
2✔
295
        }
2✔
296

297
        hasNext := totalCount > int(filter.Page*filter.PerPage)
6✔
298
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
6✔
299
        for _, l := range links {
12✔
300
                w.Header().Add("Link", l)
6✔
301
        }
6✔
302
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
6✔
303

6✔
304
        d.view.RenderSuccessGet(w, releases)
6✔
305
}
306

307
type limitResponse struct {
308
        Limit uint64 `json:"limit"`
309
        Usage uint64 `json:"usage"`
310
}
311

312
func (d *DeploymentsApiHandlers) GetLimit(w rest.ResponseWriter, r *rest.Request) {
6✔
313
        l := requestlog.GetRequestLogger(r)
6✔
314

6✔
315
        name := r.PathParam("name")
6✔
316

6✔
317
        if !model.IsValidLimit(name) {
8✔
318
                d.view.RenderError(w, r,
2✔
319
                        errors.Errorf("unsupported limit %s", name),
2✔
320
                        http.StatusBadRequest, l)
2✔
321
                return
2✔
322
        }
2✔
323

324
        limit, err := d.app.GetLimit(r.Context(), name)
4✔
325
        if err != nil {
6✔
326
                d.view.RenderInternalError(w, r, err, l)
2✔
327
                return
2✔
328
        }
2✔
329

330
        d.view.RenderSuccessGet(w, limitResponse{
2✔
331
                Limit: limit.Value,
2✔
332
                Usage: 0, // TODO fill this when ready
2✔
333
        })
2✔
334
}
335

336
// images
337

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

1✔
341
        id := r.PathParam("id")
1✔
342

1✔
343
        if !govalidator.IsUUID(id) {
2✔
344
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
345
                return
1✔
346
        }
1✔
347

348
        image, err := d.app.GetImage(r.Context(), id)
1✔
349
        if err != nil {
1✔
350
                d.view.RenderInternalError(w, r, err, l)
×
351
                return
×
352
        }
×
353

354
        if image == nil {
2✔
355
                d.view.RenderErrorNotFound(w, r, l)
1✔
356
                return
1✔
357
        }
1✔
358

359
        d.view.RenderSuccessGet(w, image)
1✔
360
}
361

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

9✔
365
        defer redactReleaseName(r)
9✔
366
        filter := getReleaseOrImageFilter(r, false)
9✔
367

9✔
368
        list, _, err := d.app.ListImages(r.Context(), filter)
9✔
369
        if err != nil {
11✔
370
                d.view.RenderInternalError(w, r, err, l)
2✔
371
                return
2✔
372
        }
2✔
373

374
        d.view.RenderSuccessGet(w, list)
7✔
375
}
376

377
func (d *DeploymentsApiHandlers) ListImages(w rest.ResponseWriter, r *rest.Request) {
8✔
378
        l := requestlog.GetRequestLogger(r)
8✔
379

8✔
380
        defer redactReleaseName(r)
8✔
381
        filter := getReleaseOrImageFilter(r, true)
8✔
382

8✔
383
        list, totalCount, err := d.app.ListImages(r.Context(), filter)
8✔
384
        if err != nil {
10✔
385
                d.view.RenderInternalError(w, r, err, l)
2✔
386
                return
2✔
387
        }
2✔
388

389
        hasNext := totalCount > int(filter.Page*filter.PerPage)
6✔
390
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
6✔
391
        for _, l := range links {
12✔
392
                w.Header().Add("Link", l)
6✔
393
        }
6✔
394
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
6✔
395

6✔
396
        d.view.RenderSuccessGet(w, list)
6✔
397
}
398

399
func (d *DeploymentsApiHandlers) DownloadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
400
        l := requestlog.GetRequestLogger(r)
1✔
401

1✔
402
        id := r.PathParam("id")
1✔
403

1✔
404
        if !govalidator.IsUUID(id) {
1✔
405
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
406
                return
×
407
        }
×
408

409
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
410
        link, err := d.app.DownloadLink(r.Context(), id, time.Duration(expireSeconds)*time.Second)
1✔
411
        if err != nil {
1✔
412
                d.view.RenderInternalError(w, r, err, l)
×
413
                return
×
414
        }
×
415

416
        if link == nil {
1✔
417
                d.view.RenderErrorNotFound(w, r, l)
×
418
                return
×
419
        }
×
420

421
        d.view.RenderSuccessGet(w, link)
1✔
422
}
423

424
func (d *DeploymentsApiHandlers) DownloadConfiguration(w rest.ResponseWriter, r *rest.Request) {
19✔
425
        if d.config.PresignSecret == nil {
21✔
426
                rest.NotFound(w, r)
2✔
427
                return
2✔
428
        }
2✔
429
        var (
17✔
430
                deviceID, _     = url.PathUnescape(r.PathParam(ParamDeviceID))
17✔
431
                deviceType, _   = url.PathUnescape(r.PathParam(ParamDeviceType))
17✔
432
                deploymentID, _ = url.PathUnescape(r.PathParam(ParamDeploymentID))
17✔
433
        )
17✔
434
        if deviceID == "" || deviceType == "" || deploymentID == "" {
17✔
435
                rest.NotFound(w, r)
×
436
                return
×
437
        }
×
438

439
        var (
17✔
440
                tenantID string
17✔
441
                l        = log.FromContext(r.Context())
17✔
442
                q        = r.URL.Query()
17✔
443
                err      error
17✔
444
        )
17✔
445
        tenantID = q.Get(ParamTenantID)
17✔
446
        sig := model.NewRequestSignature(r.Request, d.config.PresignSecret)
17✔
447
        if err = sig.Validate(); err != nil {
22✔
448
                switch cause := errors.Cause(err); cause {
5✔
449
                case model.ErrLinkExpired:
2✔
450
                        d.view.RenderError(w, r, cause, http.StatusForbidden, l)
2✔
451
                default:
3✔
452
                        d.view.RenderError(w, r,
3✔
453
                                errors.Wrap(err, "invalid request parameters"),
3✔
454
                                http.StatusBadRequest, l,
3✔
455
                        )
3✔
456
                }
457
                return
5✔
458
        }
459

460
        if !sig.VerifyHMAC256() {
16✔
461
                d.view.RenderError(w, r,
3✔
462
                        errors.New("signature invalid"),
3✔
463
                        http.StatusForbidden, l,
3✔
464
                )
3✔
465
                return
3✔
466
        }
3✔
467

468
        // Validate request signature
469
        ctx := identity.WithContext(r.Context(), &identity.Identity{
11✔
470
                Subject:  deviceID,
11✔
471
                Tenant:   tenantID,
11✔
472
                IsDevice: true,
11✔
473
        })
11✔
474

11✔
475
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
11✔
476
        if err != nil {
15✔
477
                switch cause := errors.Cause(err); cause {
4✔
478
                case app.ErrModelDeploymentNotFound:
2✔
479
                        d.view.RenderError(w, r,
2✔
480
                                errors.Errorf(
2✔
481
                                        "deployment with id '%s' not found",
2✔
482
                                        deploymentID,
2✔
483
                                ),
2✔
484
                                http.StatusNotFound, l,
2✔
485
                        )
2✔
486
                default:
2✔
487
                        l.Error(err.Error())
2✔
488
                        d.view.RenderInternalError(w, r, err, l)
2✔
489
                }
490
                return
4✔
491
        }
492
        artifactPayload, err := io.ReadAll(artifact)
7✔
493
        if err != nil {
9✔
494
                l.Error(err.Error())
2✔
495
                d.view.RenderInternalError(w, r, err, l)
2✔
496
                return
2✔
497
        }
2✔
498

499
        rw := w.(http.ResponseWriter)
5✔
500
        hdr := rw.Header()
5✔
501
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
5✔
502
        hdr.Set("Content-Type", app.ArtifactContentType)
5✔
503
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
5✔
504
        rw.WriteHeader(http.StatusOK)
5✔
505
        _, err = rw.Write(artifactPayload)
5✔
506
        if err != nil {
5✔
507
                // There's not anything we can do here in terms of the response.
×
508
                l.Error(err.Error())
×
509
        }
×
510
}
511

512
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
1✔
513
        l := requestlog.GetRequestLogger(r)
1✔
514

1✔
515
        id := r.PathParam("id")
1✔
516

1✔
517
        if !govalidator.IsUUID(id) {
1✔
518
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
519
                return
×
520
        }
×
521

522
        if err := d.app.DeleteImage(r.Context(), id); err != nil {
2✔
523
                switch err {
1✔
524
                default:
×
525
                        d.view.RenderInternalError(w, r, err, l)
×
526
                case app.ErrImageMetaNotFound:
×
527
                        d.view.RenderErrorNotFound(w, r, l)
×
528
                case app.ErrModelImageInActiveDeployment:
1✔
529
                        d.view.RenderError(w, r, ErrArtifactUsedInActiveDeployment, http.StatusConflict, l)
1✔
530
                }
531
                return
1✔
532
        }
533

534
        d.view.RenderSuccessDelete(w)
1✔
535
}
536

537
func (d *DeploymentsApiHandlers) EditImage(w rest.ResponseWriter, r *rest.Request) {
×
538
        l := requestlog.GetRequestLogger(r)
×
539

×
540
        id := r.PathParam("id")
×
541

×
542
        if !govalidator.IsUUID(id) {
×
543
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
544
                return
×
545
        }
×
546

547
        constructor, err := getImageMetaFromBody(r)
×
548
        if err != nil {
×
549
                d.view.RenderError(
×
550
                        w,
×
551
                        r,
×
552
                        errors.Wrap(err, "Validating request body"),
×
553
                        http.StatusBadRequest,
×
554
                        l,
×
555
                )
×
556
                return
×
557
        }
×
558

559
        found, err := d.app.EditImage(r.Context(), id, constructor)
×
560
        if err != nil {
×
561
                if err == app.ErrModelImageUsedInAnyDeployment {
×
562
                        d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
563
                        return
×
564
                }
×
565
                d.view.RenderInternalError(w, r, err, l)
×
566
                return
×
567
        }
568

569
        if !found {
×
570
                d.view.RenderErrorNotFound(w, r, l)
×
571
                return
×
572
        }
×
573

574
        d.view.RenderSuccessPut(w)
×
575
}
576

577
func getImageMetaFromBody(r *rest.Request) (*model.ImageMeta, error) {
×
578

×
579
        var constructor *model.ImageMeta
×
580

×
581
        if err := r.DecodeJsonPayload(&constructor); err != nil {
×
582
                return nil, err
×
583
        }
×
584

585
        if err := constructor.Validate(); err != nil {
×
586
                return nil, err
×
587
        }
×
588

589
        return constructor, nil
×
590
}
591

592
// NewImage is the Multipart Image/Meta upload handler.
593
// Request should be of type "multipart/form-data". The parts are
594
// key/valyue pairs of metadata information except the last one,
595
// which must contain the artifact file.
596
func (d *DeploymentsApiHandlers) NewImage(w rest.ResponseWriter, r *rest.Request) {
13✔
597
        d.newImageWithContext(r.Context(), w, r)
13✔
598
}
13✔
599

600
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(w rest.ResponseWriter, r *rest.Request) {
13✔
601
        l := requestlog.GetRequestLogger(r)
13✔
602

13✔
603
        tenantID := r.PathParam("tenant")
13✔
604

13✔
605
        if tenantID == "" {
13✔
606
                rest_utils.RestErrWithLog(
×
607
                        w,
×
608
                        r,
×
609
                        l,
×
610
                        fmt.Errorf("missing tenant id in path"),
×
611
                        http.StatusBadRequest,
×
612
                )
×
613
                return
×
614
        }
×
615

616
        var ctx context.Context
13✔
617
        if tenantID != "default" {
14✔
618
                ident := &identity.Identity{Tenant: tenantID}
1✔
619
                ctx = identity.WithContext(r.Context(), ident)
1✔
620
        } else {
13✔
621
                ctx = r.Context()
12✔
622
        }
12✔
623

624
        d.newImageWithContext(ctx, w, r)
13✔
625
}
626

627
func (d *DeploymentsApiHandlers) newImageWithContext(
628
        ctx context.Context,
629
        w rest.ResponseWriter,
630
        r *rest.Request,
631
) {
25✔
632
        l := requestlog.GetRequestLogger(r)
25✔
633

25✔
634
        formReader, err := r.MultipartReader()
25✔
635
        if err != nil {
33✔
636
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
8✔
637
                return
8✔
638
        }
8✔
639

640
        // parse multipart message
641
        multipartUploadMsg, err := d.ParseMultipart(formReader)
17✔
642

17✔
643
        if err != nil {
26✔
644
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
9✔
645
                return
9✔
646
        }
9✔
647

648
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
9✔
649
        if err == nil {
14✔
650
                d.view.RenderSuccessPost(w, r, imgID)
5✔
651
                return
5✔
652
        }
5✔
653
        if cErr, ok := err.(*model.ConflictError); ok {
9✔
654
                d.view.RenderError(w, r, cErr, http.StatusConflict, l)
4✔
655
                return
4✔
656
        }
4✔
657
        cause := errors.Cause(err)
1✔
658
        switch cause {
1✔
659
        default:
×
660
                d.view.RenderInternalError(w, r, err, l)
×
661
                return
×
662
        case app.ErrModelArtifactNotUnique:
×
663
                l.Error(err.Error())
×
664
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
665
                return
×
666
        case app.ErrModelParsingArtifactFailed:
1✔
667
                l.Error(err.Error())
1✔
668
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
669
                return
1✔
670
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
671
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
672
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
673
                l.Error(err.Error())
×
674
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
675
                return
×
676
        }
677
}
678

679
func formatArtifactUploadError(err error) error {
3✔
680
        // remove generic message
3✔
681
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
3✔
682

3✔
683
        // handle specific cases
3✔
684

3✔
685
        if strings.Contains(errMsg, "invalid checksum") {
3✔
686
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
687
        }
×
688

689
        if strings.Contains(errMsg, "unsupported version") {
3✔
690
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
691
                        "; supported versions are: 1, 2")
×
692
        }
×
693

694
        return errors.New(errMsg)
3✔
695
}
696

697
// GenerateImage s the multipart Raw Data/Meta upload handler.
698
// Request should be of type "multipart/form-data". The parts are
699
// key/valyue pairs of metadata information except the last one,
700
// which must contain the file containing the raw data to be processed
701
// into an artifact.
702
func (d *DeploymentsApiHandlers) GenerateImage(w rest.ResponseWriter, r *rest.Request) {
25✔
703
        l := requestlog.GetRequestLogger(r)
25✔
704

25✔
705
        formReader, err := r.MultipartReader()
25✔
706
        if err != nil {
29✔
707
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
708
                return
4✔
709
        }
4✔
710

711
        // parse multipart message
712
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
21✔
713
        if err != nil {
29✔
714
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
8✔
715
                return
8✔
716
        }
8✔
717

718
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
13✔
719
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
25✔
720
                multipartMsg.Token = tokenFields[1]
12✔
721
        }
12✔
722

723
        imgID, err := d.app.GenerateImage(r.Context(), multipartMsg)
13✔
724
        cause := errors.Cause(err)
13✔
725
        switch cause {
13✔
726
        default:
2✔
727
                d.view.RenderInternalError(w, r, err, l)
2✔
728
        case nil:
5✔
729
                d.view.RenderSuccessPost(w, r, imgID)
5✔
730
        case app.ErrModelArtifactNotUnique:
2✔
731
                l.Error(err.Error())
2✔
732
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
2✔
733
        case app.ErrModelParsingArtifactFailed:
2✔
734
                l.Error(err.Error())
2✔
735
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
2✔
736
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
737
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
738
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
2✔
739
                l.Error(err.Error())
2✔
740
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
2✔
741
        }
742
}
743

744
// ParseMultipart parses multipart/form-data message.
745
func (d *DeploymentsApiHandlers) ParseMultipart(
746
        r *multipart.Reader,
747
) (*model.MultipartUploadMsg, error) {
17✔
748
        uploadMsg := &model.MultipartUploadMsg{
17✔
749
                MetaConstructor: &model.ImageMeta{},
17✔
750
        }
17✔
751
        var size int64
17✔
752
        // Parse the multipart form sequentially. To remain backward compatible
17✔
753
        // all form names that are not part of the API are ignored.
17✔
754
        for {
74✔
755
                part, err := r.NextPart()
57✔
756
                if err != nil {
61✔
757
                        if err == io.EOF {
8✔
758
                                // The whole message has been consumed without
4✔
759
                                // the "artifact" form part.
4✔
760
                                return nil, ErrArtifactFileMissing
4✔
761
                        }
4✔
762
                        return nil, err
×
763
                }
764
                switch strings.ToLower(part.FormName()) {
53✔
765
                case "description":
13✔
766
                        // Add description to the metadata
13✔
767
                        dscr, err := io.ReadAll(part)
13✔
768
                        if err != nil {
13✔
769
                                return nil, err
×
770
                        }
×
771
                        uploadMsg.MetaConstructor.Description = string(dscr)
13✔
772

773
                case "size":
13✔
774
                        // Add size limit to the metadata
13✔
775
                        sz, err := io.ReadAll(part)
13✔
776
                        if err != nil {
13✔
777
                                return nil, err
×
778
                        }
×
779
                        size, err = strconv.ParseInt(string(sz), 10, 64)
13✔
780
                        if err != nil {
13✔
781
                                return nil, err
×
782
                        }
×
783
                        // Add one since this will impose the upper limit on the
784
                        // artifact size.
785
                        if size > d.config.MaxImageSize {
13✔
786
                                return nil, ErrModelArtifactFileTooLarge
×
787
                        }
×
788

789
                case "artifact_id":
13✔
790
                        // Add artifact id to the metadata (must be a valid UUID).
13✔
791
                        b, err := io.ReadAll(part)
13✔
792
                        if err != nil {
13✔
793
                                return nil, err
×
794
                        }
×
795
                        id := string(b)
13✔
796
                        if !govalidator.IsUUID(id) {
18✔
797
                                return nil, errors.New(
5✔
798
                                        "artifact_id is not a valid UUID",
5✔
799
                                )
5✔
800
                        }
5✔
801
                        uploadMsg.ArtifactID = id
8✔
802

803
                case "artifact":
9✔
804
                        // Assign the form-data payload to the artifact reader
9✔
805
                        // and return. The content is consumed elsewhere.
9✔
806
                        if size > 0 {
18✔
807
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
9✔
808
                        } else {
9✔
809
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
×
810
                                        part,
×
811
                                        d.config.MaxImageSize,
×
812
                                )
×
813
                        }
×
814
                        return uploadMsg, nil
9✔
815

816
                default:
8✔
817
                        // Ignore all non-API sections.
8✔
818
                        continue
8✔
819
                }
820
        }
821
}
822

823
// ParseGenerateImageMultipart parses multipart/form-data message.
824
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
825
        r *multipart.Reader,
826
) (*model.MultipartGenerateImageMsg, error) {
21✔
827
        msg := &model.MultipartGenerateImageMsg{}
21✔
828
        var size int64
21✔
829

21✔
830
ParseLoop:
21✔
831
        for {
128✔
832
                part, err := r.NextPart()
107✔
833
                if err != nil {
111✔
834
                        if err == io.EOF {
8✔
835
                                break
4✔
836
                        }
837
                        return nil, err
×
838
                }
839
                switch strings.ToLower(part.FormName()) {
103✔
840
                case "args":
13✔
841
                        b, err := io.ReadAll(part)
13✔
842
                        if err != nil {
13✔
843
                                return nil, errors.Wrap(err,
×
844
                                        "failed to read form value 'args'",
×
845
                                )
×
846
                        }
×
847
                        msg.Args = string(b)
13✔
848

849
                case "description":
13✔
850
                        b, err := io.ReadAll(part)
13✔
851
                        if err != nil {
13✔
852
                                return nil, errors.Wrap(err,
×
853
                                        "failed to read form value 'description'",
×
854
                                )
×
855
                        }
×
856
                        msg.Description = string(b)
13✔
857

858
                case "device_types_compatible":
17✔
859
                        b, err := io.ReadAll(part)
17✔
860
                        if err != nil {
17✔
861
                                return nil, errors.Wrap(err,
×
862
                                        "failed to read form value 'device_types_compatible'",
×
863
                                )
×
864
                        }
×
865
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
17✔
866

867
                case "file":
17✔
868
                        if size > 0 {
29✔
869
                                msg.FileReader = utils.ReadExactly(part, size)
12✔
870
                        } else {
17✔
871
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxImageSize)
5✔
872
                        }
5✔
873
                        break ParseLoop
17✔
874

875
                case "name":
19✔
876
                        b, err := io.ReadAll(part)
19✔
877
                        if err != nil {
19✔
878
                                return nil, errors.Wrap(err,
×
879
                                        "failed to read form value 'name'",
×
880
                                )
×
881
                        }
×
882
                        msg.Name = string(b)
19✔
883

884
                case "type":
17✔
885
                        b, err := io.ReadAll(part)
17✔
886
                        if err != nil {
17✔
887
                                return nil, errors.Wrap(err,
×
888
                                        "failed to read form value 'type'",
×
889
                                )
×
890
                        }
×
891
                        msg.Type = string(b)
17✔
892

893
                case "size":
12✔
894
                        // Add size limit to the metadata
12✔
895
                        sz, err := io.ReadAll(part)
12✔
896
                        if err != nil {
12✔
897
                                return nil, err
×
898
                        }
×
899
                        size, err = strconv.ParseInt(string(sz), 10, 64)
12✔
900
                        if err != nil {
12✔
901
                                return nil, err
×
902
                        }
×
903
                        // Add one since this will impose the upper limit on the
904
                        // artifact size.
905
                        if size > d.config.MaxImageSize {
12✔
906
                                return nil, ErrModelArtifactFileTooLarge
×
907
                        }
×
908

909
                default:
×
910
                        // Ignore non-API sections.
×
911
                        continue
×
912
                }
913
        }
914

915
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
21✔
916
}
917

918
// deployments
919
func (d *DeploymentsApiHandlers) createDeployment(
920
        w rest.ResponseWriter,
921
        r *rest.Request,
922
        ctx context.Context,
923
        l *log.Logger,
924
        group string,
925
) {
25✔
926
        constructor, err := d.getDeploymentConstructorFromBody(r, group)
25✔
927
        if err != nil {
36✔
928
                d.view.RenderError(
11✔
929
                        w,
11✔
930
                        r,
11✔
931
                        errors.Wrap(err, "Validating request body"),
11✔
932
                        http.StatusBadRequest,
11✔
933
                        l,
11✔
934
                )
11✔
935
                return
11✔
936
        }
11✔
937

938
        id, err := d.app.CreateDeployment(ctx, constructor)
15✔
939
        switch err {
15✔
940
        case nil:
7✔
941
                // in case of deployment to group remove "/group/{name}" from path before creating location
7✔
942
                // haeder
7✔
943
                r.URL.Path = strings.TrimSuffix(r.URL.Path, "/group/"+constructor.Group)
7✔
944
                d.view.RenderSuccessPost(w, r, id)
7✔
945
        case app.ErrNoArtifact:
1✔
946
                d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
1✔
947
        case app.ErrNoDevices:
4✔
948
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
949
        default:
4✔
950
                d.view.RenderInternalError(w, r, err, l)
4✔
951
        }
952
}
953

954
func (d *DeploymentsApiHandlers) PostDeployment(w rest.ResponseWriter, r *rest.Request) {
15✔
955
        ctx := r.Context()
15✔
956
        l := requestlog.GetRequestLogger(r)
15✔
957

15✔
958
        d.createDeployment(w, r, ctx, l, "")
15✔
959
}
15✔
960

961
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
10✔
962
        ctx := r.Context()
10✔
963
        l := requestlog.GetRequestLogger(r)
10✔
964

10✔
965
        group := r.PathParam("name")
10✔
966
        if len(group) < 1 {
10✔
967
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
968
        }
×
969
        d.createDeployment(w, r, ctx, l, group)
10✔
970
}
971

972
// parseDeviceConfigurationDeploymentPathParams parses expected params
973
// and check if the params are not empty
974
func parseDeviceConfigurationDeploymentPathParams(r *rest.Request) (string, string, string, error) {
13✔
975
        tenantID := r.PathParam("tenant")
13✔
976
        deviceID := r.PathParam(ParamDeviceID)
13✔
977
        if deviceID == "" {
13✔
978
                return "", "", "", errors.New("device ID missing")
×
979
        }
×
980
        deploymentID := r.PathParam(ParamDeploymentID)
13✔
981
        if deploymentID == "" {
13✔
982
                return "", "", "", errors.New("deployment ID missing")
×
983
        }
×
984
        return tenantID, deviceID, deploymentID, nil
13✔
985
}
986

987
// getConfigurationDeploymentConstructorFromBody extracts configuration
988
// deployment constructor from the request body and validates it
989
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
990
        *model.ConfigurationDeploymentConstructor, error) {
13✔
991

13✔
992
        var constructor *model.ConfigurationDeploymentConstructor
13✔
993

13✔
994
        if err := r.DecodeJsonPayload(&constructor); err != nil {
15✔
995
                return nil, err
2✔
996
        }
2✔
997

998
        if err := constructor.Validate(); err != nil {
14✔
999
                return nil, err
3✔
1000
        }
3✔
1001

1002
        return constructor, nil
9✔
1003
}
1004

1005
// device configuration deployment handler
1006
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1007
        w rest.ResponseWriter,
1008
        r *rest.Request,
1009
) {
13✔
1010
        l := requestlog.GetRequestLogger(r)
13✔
1011

13✔
1012
        // get path params
13✔
1013
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
13✔
1014
        if err != nil {
13✔
1015
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
1016
                return
×
1017
        }
×
1018

1019
        // add tenant id to the context
1020
        ctx := identity.WithContext(r.Context(), &identity.Identity{Tenant: tenantID})
13✔
1021

13✔
1022
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
13✔
1023
        if err != nil {
18✔
1024
                d.view.RenderError(
5✔
1025
                        w,
5✔
1026
                        r,
5✔
1027
                        errors.Wrap(err, "Validating request body"),
5✔
1028
                        http.StatusBadRequest,
5✔
1029
                        l,
5✔
1030
                )
5✔
1031
                return
5✔
1032
        }
5✔
1033

1034
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
9✔
1035
        switch err {
9✔
1036
        default:
2✔
1037
                d.view.RenderInternalError(w, r, err, l)
2✔
1038
        case nil:
5✔
1039
                r.URL.Path = "./deployments"
5✔
1040
                d.view.RenderSuccessPost(w, r, id)
5✔
1041
        case app.ErrDuplicateDeployment:
3✔
1042
                d.view.RenderError(w, r, err, http.StatusConflict, l)
3✔
1043
        case app.ErrInvalidDeploymentID:
1✔
1044
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1045
        }
1046
}
1047

1048
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1049
        r *rest.Request,
1050
        group string,
1051
) (*model.DeploymentConstructor, error) {
25✔
1052
        var constructor *model.DeploymentConstructor
25✔
1053
        if err := r.DecodeJsonPayload(&constructor); err != nil {
30✔
1054
                return nil, err
5✔
1055
        }
5✔
1056

1057
        constructor.Group = group
21✔
1058

21✔
1059
        if err := constructor.ValidateNew(); err != nil {
28✔
1060
                return nil, err
7✔
1061
        }
7✔
1062

1063
        return constructor, nil
15✔
1064
}
1065

1066
func (d *DeploymentsApiHandlers) GetDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1067
        ctx := r.Context()
1✔
1068
        l := requestlog.GetRequestLogger(r)
1✔
1069

1✔
1070
        id := r.PathParam("id")
1✔
1071

1✔
1072
        if !govalidator.IsUUID(id) {
2✔
1073
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
1074
                return
1✔
1075
        }
1✔
1076

1077
        deployment, err := d.app.GetDeployment(ctx, id)
1✔
1078
        if err != nil {
1✔
1079
                d.view.RenderInternalError(w, r, err, l)
×
1080
                return
×
1081
        }
×
1082

1083
        if deployment == nil {
1✔
1084
                d.view.RenderErrorNotFound(w, r, l)
×
1085
                return
×
1086
        }
×
1087

1088
        d.view.RenderSuccessGet(w, deployment)
1✔
1089
}
1090

1091
func (d *DeploymentsApiHandlers) GetDeploymentStats(w rest.ResponseWriter, r *rest.Request) {
1✔
1092
        ctx := r.Context()
1✔
1093
        l := requestlog.GetRequestLogger(r)
1✔
1094

1✔
1095
        id := r.PathParam("id")
1✔
1096

1✔
1097
        if !govalidator.IsUUID(id) {
1✔
1098
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1099
                return
×
1100
        }
×
1101

1102
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1103
        if err != nil {
1✔
1104
                d.view.RenderInternalError(w, r, err, l)
×
1105
                return
×
1106
        }
×
1107

1108
        if stats == nil {
1✔
1109
                d.view.RenderErrorNotFound(w, r, l)
×
1110
                return
×
1111
        }
×
1112

1113
        d.view.RenderSuccessGet(w, stats)
1✔
1114
}
1115

1116
func (d *DeploymentsApiHandlers) GetDeploymentsStats(w rest.ResponseWriter, r *rest.Request) {
8✔
1117

8✔
1118
        ctx := r.Context()
8✔
1119
        l := requestlog.GetRequestLogger(r)
8✔
1120

8✔
1121
        ids := model.DeploymentIDs{}
8✔
1122
        if err := r.DecodeJsonPayload(&ids); err != nil {
8✔
1123
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1124
                return
×
1125
        }
×
1126

1127
        if len(ids.IDs) == 0 {
8✔
1128
                w.WriteHeader(http.StatusOK)
×
1129
                _ = w.WriteJson(struct{}{})
×
1130
                return
×
1131
        }
×
1132

1133
        if err := ids.Validate(); err != nil {
10✔
1134
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1135
                return
2✔
1136
        }
2✔
1137

1138
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
6✔
1139
        if err != nil {
10✔
1140
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
6✔
1141
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
2✔
1142
                        return
2✔
1143
                }
2✔
1144
                d.view.RenderInternalError(w, r, err, l)
2✔
1145
                return
2✔
1146
        }
1147

1148
        w.WriteHeader(http.StatusOK)
2✔
1149

2✔
1150
        _ = w.WriteJson(stats)
2✔
1151
}
1152

1153
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
1154
        ctx := r.Context()
×
1155
        l := requestlog.GetRequestLogger(r)
×
1156

×
1157
        id := r.PathParam("id")
×
1158

×
1159
        if !govalidator.IsUUID(id) {
×
1160
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1161
                return
×
1162
        }
×
1163

1164
        deployment, err := d.app.GetDeployment(ctx, id)
×
1165
        if err != nil {
×
1166
                d.view.RenderInternalError(w, r, err, l)
×
1167
                return
×
1168
        }
×
1169

1170
        if deployment == nil {
×
1171
                d.view.RenderErrorNotFound(w, r, l)
×
1172
                return
×
1173
        }
×
1174

1175
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1176
}
1177

1178
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1179
        ctx := r.Context()
1✔
1180
        l := requestlog.GetRequestLogger(r)
1✔
1181

1✔
1182
        id := r.PathParam("id")
1✔
1183

1✔
1184
        if !govalidator.IsUUID(id) {
1✔
1185
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1186
                return
×
1187
        }
×
1188

1189
        // receive request body
1190
        var status struct {
1✔
1191
                Status model.DeviceDeploymentStatus
1✔
1192
        }
1✔
1193

1✔
1194
        err := r.DecodeJsonPayload(&status)
1✔
1195
        if err != nil {
1✔
1196
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1197
                return
×
1198
        }
×
1199
        // "aborted" is the only supported status
1200
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1201
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
1202
        }
×
1203

1204
        l.Infof("Abort deployment: %s", id)
1✔
1205

1✔
1206
        // Check if deployment is finished
1✔
1207
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1208
        if err != nil {
1✔
1209
                d.view.RenderInternalError(w, r, err, l)
×
1210
                return
×
1211
        }
×
1212
        if isDeploymentFinished {
2✔
1213
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
1✔
1214
                return
1✔
1215
        }
1✔
1216

1217
        // Abort deployments for devices and update deployment stats
1218
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1219
                d.view.RenderInternalError(w, r, err, l)
×
1220
        }
×
1221

1222
        d.view.RenderEmptySuccessResponse(w)
1✔
1223
}
1224

1225
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
23✔
1226
        var (
23✔
1227
                installed *model.InstalledDeviceDeployment
23✔
1228
                ctx       = r.Context()
23✔
1229
                l         = requestlog.GetRequestLogger(r)
23✔
1230
                idata     = identity.FromContext(ctx)
23✔
1231
        )
23✔
1232
        if idata == nil {
26✔
1233
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
3✔
1234
                return
3✔
1235
        }
3✔
1236

1237
        q := r.URL.Query()
21✔
1238
        defer func() {
42✔
1239
                var reEncode bool = false
21✔
1240
                if name := q.Get(ParamArtifactName); name != "" {
38✔
1241
                        q.Set(ParamArtifactName, Redacted)
17✔
1242
                        reEncode = true
17✔
1243
                }
17✔
1244
                if typ := q.Get(ParamDeviceType); typ != "" {
38✔
1245
                        q.Set(ParamDeviceType, Redacted)
17✔
1246
                        reEncode = true
17✔
1247
                }
17✔
1248
                if reEncode {
38✔
1249
                        r.URL.RawQuery = q.Encode()
17✔
1250
                }
17✔
1251
        }()
1252
        if strings.EqualFold(r.Method, http.MethodPost) {
25✔
1253
                // POST
4✔
1254
                installed = new(model.InstalledDeviceDeployment)
4✔
1255
                if err := r.DecodeJsonPayload(&installed); err != nil {
6✔
1256
                        d.view.RenderError(w, r,
2✔
1257
                                errors.Wrap(err, "invalid schema"),
2✔
1258
                                http.StatusBadRequest, l)
2✔
1259
                        return
2✔
1260
                }
2✔
1261
        } else {
17✔
1262
                // GET or HEAD
17✔
1263
                installed = &model.InstalledDeviceDeployment{
17✔
1264
                        ArtifactName: q.Get(ParamArtifactName),
17✔
1265
                        DeviceType:   q.Get(ParamDeviceType),
17✔
1266
                }
17✔
1267
        }
17✔
1268

1269
        if err := installed.Validate(); err != nil {
21✔
1270
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1271
                return
2✔
1272
        }
2✔
1273

1274
        request := &model.DeploymentNextRequest{
17✔
1275
                DeviceProvides: installed,
17✔
1276
        }
17✔
1277

17✔
1278
        d.getDeploymentForDevice(w, r, idata, request)
17✔
1279
}
1280

1281
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1282
        w rest.ResponseWriter,
1283
        r *rest.Request,
1284
        idata *identity.Identity,
1285
        request *model.DeploymentNextRequest,
1286
) {
17✔
1287
        ctx := r.Context()
17✔
1288
        l := requestlog.GetRequestLogger(r)
17✔
1289

17✔
1290
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
17✔
1291
        if err != nil {
20✔
1292
                if err == app.ErrConflictingRequestData {
4✔
1293
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1294
                } else {
3✔
1295
                        d.view.RenderInternalError(w, r, err, l)
2✔
1296
                }
2✔
1297
                return
3✔
1298
        }
1299

1300
        if deployment == nil {
18✔
1301
                d.view.RenderNoUpdateForDevice(w)
3✔
1302
                return
3✔
1303
        } else if deployment.Type == model.DeploymentTypeConfiguration {
25✔
1304
                // Generate pre-signed URL
9✔
1305
                var hostName string = d.config.PresignHostname
9✔
1306
                if hostName == "" {
13✔
1307
                        if hostName = r.Header.Get(hdrForwardedHost); hostName == "" {
6✔
1308
                                d.view.RenderInternalError(w, r,
2✔
1309
                                        errors.New("presign.hostname not configured; "+
2✔
1310
                                                "unable to generate download link "+
2✔
1311
                                                " for configuration deployment"), l)
2✔
1312
                                return
2✔
1313
                        }
2✔
1314
                }
1315
                req, _ := http.NewRequest(
7✔
1316
                        http.MethodGet,
7✔
1317
                        FMTConfigURL(
7✔
1318
                                d.config.PresignScheme, hostName,
7✔
1319
                                deployment.ID, request.DeviceProvides.DeviceType,
7✔
1320
                                idata.Subject,
7✔
1321
                        ),
7✔
1322
                        nil,
7✔
1323
                )
7✔
1324
                if idata.Tenant != "" {
12✔
1325
                        q := req.URL.Query()
5✔
1326
                        q.Set(model.ParamTenantID, idata.Tenant)
5✔
1327
                        req.URL.RawQuery = q.Encode()
5✔
1328
                }
5✔
1329
                sig := model.NewRequestSignature(req, d.config.PresignSecret)
7✔
1330
                expireTS := time.Now().Add(d.config.PresignExpire)
7✔
1331
                sig.SetExpire(expireTS)
7✔
1332
                deployment.Artifact.Source = model.Link{
7✔
1333
                        Uri:    sig.PresignURL(),
7✔
1334
                        Expire: expireTS,
7✔
1335
                }
7✔
1336
        }
1337

1338
        d.view.RenderSuccessGet(w, deployment)
11✔
1339
}
1340

1341
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1342
        w rest.ResponseWriter,
1343
        r *rest.Request,
1344
) {
1✔
1345
        ctx := r.Context()
1✔
1346
        l := requestlog.GetRequestLogger(r)
1✔
1347

1✔
1348
        did := r.PathParam("id")
1✔
1349

1✔
1350
        idata := identity.FromContext(ctx)
1✔
1351
        if idata == nil {
1✔
1352
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1353
                return
×
1354
        }
×
1355

1356
        // receive request body
1357
        var report model.StatusReport
1✔
1358

1✔
1359
        err := r.DecodeJsonPayload(&report)
1✔
1360
        if err != nil {
1✔
1361
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1362
                return
×
1363
        }
×
1364

1365
        l.Infof("status: %+v", report)
1✔
1366
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
1✔
1367
                idata.Subject, model.DeviceDeploymentState{
1✔
1368
                        Status:   report.Status,
1✔
1369
                        SubState: report.SubState,
1✔
1370
                }); err != nil {
1✔
1371

×
1372
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
1373
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
1374
                } else if err == app.ErrStorageNotFound {
×
1375
                        d.view.RenderErrorNotFound(w, r, l)
×
1376
                } else {
×
1377
                        d.view.RenderInternalError(w, r, err, l)
×
1378
                }
×
1379
                return
×
1380
        }
1381

1382
        d.view.RenderEmptySuccessResponse(w)
1✔
1383
}
1384

1385
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1386
        w rest.ResponseWriter,
1387
        r *rest.Request,
1388
) {
1✔
1389
        ctx := r.Context()
1✔
1390
        l := requestlog.GetRequestLogger(r)
1✔
1391

1✔
1392
        did := r.PathParam("id")
1✔
1393

1✔
1394
        if !govalidator.IsUUID(did) {
1✔
1395
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1396
                return
×
1397
        }
×
1398

1399
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
1✔
1400
        if err != nil {
1✔
1401
                switch err {
×
1402
                case app.ErrModelDeploymentNotFound:
×
1403
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1404
                        return
×
1405
                default:
×
1406
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1407
                        return
×
1408
                }
1409
        }
1410

1411
        d.view.RenderSuccessGet(w, statuses)
1✔
1412
}
1413

1414
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1415
        w rest.ResponseWriter,
1416
        r *rest.Request,
1417
) {
1✔
1418
        ctx := r.Context()
1✔
1419
        l := requestlog.GetRequestLogger(r)
1✔
1420

1✔
1421
        did := r.PathParam("id")
1✔
1422

1✔
1423
        if !govalidator.IsUUID(did) {
1✔
1424
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1425
                return
×
1426
        }
×
1427

1428
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1429
        if err != nil {
1✔
1430
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1431
                return
×
1432
        }
×
1433

1434
        lq := store.ListQuery{
1✔
1435
                Skip:         int((page - 1) * perPage),
1✔
1436
                Limit:        int(perPage),
1✔
1437
                DeploymentID: did,
1✔
1438
        }
1✔
1439
        if status := r.URL.Query().Get("status"); status != "" {
1✔
1440
                lq.Status = &status
×
1441
        }
×
1442
        if err = lq.Validate(); err != nil {
1✔
1443
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1444
                return
×
1445
        }
×
1446

1447
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1448
        if err != nil {
1✔
1449
                switch err {
×
1450
                case app.ErrModelDeploymentNotFound:
×
1451
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1452
                        return
×
1453
                default:
×
1454
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1455
                        return
×
1456
                }
1457
        }
1458

1459
        hasNext := totalCount > int(page*perPage)
1✔
1460
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1461
        for _, l := range links {
2✔
1462
                w.Header().Add("Link", l)
1✔
1463
        }
1✔
1464
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1465
        d.view.RenderSuccessGet(w, statuses)
1✔
1466
}
1467

1468
func ParseLookupQuery(vals url.Values) (model.Query, error) {
17✔
1469
        query := model.Query{}
17✔
1470

17✔
1471
        search := vals.Get("search")
17✔
1472
        if search != "" {
17✔
1473
                query.SearchText = search
×
1474
        }
×
1475

1476
        createdBefore := vals.Get("created_before")
17✔
1477
        if createdBefore != "" {
19✔
1478
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
4✔
1479
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
2✔
1480
                } else {
2✔
1481
                        query.CreatedBefore = &createdBeforeTime
×
1482
                }
×
1483
        }
1484

1485
        createdAfter := vals.Get("created_after")
15✔
1486
        if createdAfter != "" {
15✔
1487
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1488
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1489
                } else {
×
1490
                        query.CreatedAfter = &createdAfterTime
×
1491
                }
×
1492
        }
1493

1494
        switch strings.ToLower(vals.Get("sort")) {
15✔
1495
        case model.SortDirectionAscending:
2✔
1496
                query.Sort = model.SortDirectionAscending
2✔
1497
        case "", model.SortDirectionDescending:
13✔
1498
                query.Sort = model.SortDirectionDescending
13✔
1499
        default:
×
1500
                return query, ErrInvalidSortDirection
×
1501
        }
1502

1503
        status := vals.Get("status")
15✔
1504
        switch status {
15✔
1505
        case "inprogress":
×
1506
                query.Status = model.StatusQueryInProgress
×
1507
        case "finished":
×
1508
                query.Status = model.StatusQueryFinished
×
1509
        case "pending":
×
1510
                query.Status = model.StatusQueryPending
×
1511
        case "aborted":
×
1512
                query.Status = model.StatusQueryAborted
×
1513
        case "":
15✔
1514
                query.Status = model.StatusQueryAny
15✔
1515
        default:
×
1516
                return query, errors.Errorf("unknown status %s", status)
×
1517

1518
        }
1519

1520
        dType := vals.Get("type")
15✔
1521
        if dType == "" {
30✔
1522
                return query, nil
15✔
1523
        }
15✔
1524
        deploymentType := model.DeploymentType(dType)
×
1525
        if deploymentType == model.DeploymentTypeSoftware ||
×
1526
                deploymentType == model.DeploymentTypeConfiguration {
×
1527
                query.Type = deploymentType
×
1528
        } else {
×
1529
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1530
        }
×
1531

1532
        return query, nil
×
1533
}
1534

1535
func parseEpochToTimestamp(epoch string) (time.Time, error) {
2✔
1536
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
4✔
1537
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
2✔
1538
        } else {
2✔
1539
                return time.Unix(epochInt64, 0).UTC(), nil
×
1540
        }
×
1541
}
1542

1543
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
17✔
1544
        ctx := r.Context()
17✔
1545
        l := requestlog.GetRequestLogger(r)
17✔
1546
        q := r.URL.Query()
17✔
1547
        defer func() {
34✔
1548
                if search := q.Get("search"); search != "" {
17✔
1549
                        q.Set("search", Redacted)
×
1550
                        r.URL.RawQuery = q.Encode()
×
1551
                }
×
1552
        }()
1553

1554
        query, err := ParseLookupQuery(q)
17✔
1555
        if err != nil {
19✔
1556
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1557
                return
2✔
1558
        }
2✔
1559

1560
        page, perPage, err := rest_utils.ParsePagination(r)
15✔
1561
        if err != nil {
17✔
1562
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1563
                return
2✔
1564
        }
2✔
1565
        query.Skip = int((page - 1) * perPage)
13✔
1566
        query.Limit = int(perPage + 1)
13✔
1567

13✔
1568
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
13✔
1569
        if err != nil {
15✔
1570
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1571
                return
2✔
1572
        }
2✔
1573
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
11✔
1574

11✔
1575
        len := len(deps)
11✔
1576
        hasNext := false
11✔
1577
        if uint64(len) > perPage {
11✔
1578
                hasNext = true
×
1579
                len = int(perPage)
×
1580
        }
×
1581

1582
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
11✔
1583
        for _, l := range links {
24✔
1584
                w.Header().Add("Link", l)
13✔
1585
        }
13✔
1586

1587
        d.view.RenderSuccessGet(w, deps[:len])
11✔
1588
}
1589

1590
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1591
        ctx := r.Context()
1✔
1592
        l := requestlog.GetRequestLogger(r)
1✔
1593

1✔
1594
        did := r.PathParam("id")
1✔
1595

1✔
1596
        idata := identity.FromContext(ctx)
1✔
1597
        if idata == nil {
1✔
1598
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1599
                return
×
1600
        }
×
1601

1602
        // reuse DeploymentLog, device and deployment IDs are ignored when
1603
        // (un-)marshaling DeploymentLog to/from JSON
1604
        var log model.DeploymentLog
1✔
1605

1✔
1606
        err := r.DecodeJsonPayload(&log)
1✔
1607
        if err != nil {
1✔
1608
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1609
                return
×
1610
        }
×
1611

1612
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1613
                did, log.Messages); err != nil {
1✔
1614

×
1615
                if err == app.ErrModelDeploymentNotFound {
×
1616
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1617
                } else {
×
1618
                        d.view.RenderInternalError(w, r, err, l)
×
1619
                }
×
1620
                return
×
1621
        }
1622

1623
        d.view.RenderEmptySuccessResponse(w)
1✔
1624
}
1625

1626
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1627
        ctx := r.Context()
1✔
1628
        l := requestlog.GetRequestLogger(r)
1✔
1629

1✔
1630
        did := r.PathParam("id")
1✔
1631
        devid := r.PathParam("devid")
1✔
1632

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

1✔
1635
        if err != nil {
1✔
1636
                d.view.RenderInternalError(w, r, err, l)
×
1637
                return
×
1638
        }
×
1639

1640
        if depl == nil {
1✔
1641
                d.view.RenderErrorNotFound(w, r, l)
×
1642
                return
×
1643
        }
×
1644

1645
        d.view.RenderDeploymentLog(w, *depl)
1✔
1646
}
1647

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

6✔
1652
        id := r.PathParam("id")
6✔
1653
        err := d.app.AbortDeviceDeployments(ctx, id)
6✔
1654

6✔
1655
        switch err {
6✔
1656
        case nil, app.ErrStorageNotFound:
4✔
1657
                d.view.RenderEmptySuccessResponse(w)
4✔
1658
        default:
2✔
1659
                d.view.RenderInternalError(w, r, err, l)
2✔
1660
        }
1661
}
1662

1663
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1664
        r *rest.Request) {
6✔
1665
        ctx := r.Context()
6✔
1666
        l := requestlog.GetRequestLogger(r)
6✔
1667

6✔
1668
        id := r.PathParam("id")
6✔
1669
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
6✔
1670

6✔
1671
        switch err {
6✔
1672
        case nil, app.ErrStorageNotFound:
4✔
1673
                d.view.RenderEmptySuccessResponse(w)
4✔
1674
        default:
2✔
1675
                d.view.RenderInternalError(w, r, err, l)
2✔
1676
        }
1677
}
1678

1679
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
18✔
1680
        ctx := r.Context()
18✔
1681
        d.listDeviceDeployments(ctx, w, r)
18✔
1682
}
18✔
1683

1684
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1685
        r *rest.Request) {
18✔
1686
        ctx := r.Context()
18✔
1687
        tenantID := r.PathParam("tenant")
18✔
1688
        if tenantID != "" {
36✔
1689
                ctx = identity.WithContext(r.Context(), &identity.Identity{
18✔
1690
                        Tenant:   tenantID,
18✔
1691
                        IsDevice: true,
18✔
1692
                })
18✔
1693
        }
18✔
1694
        d.listDeviceDeployments(ctx, w, r)
18✔
1695
}
1696

1697
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1698
        w rest.ResponseWriter, r *rest.Request) {
36✔
1699
        l := requestlog.GetRequestLogger(r)
36✔
1700

36✔
1701
        did := r.PathParam("id")
36✔
1702
        if !govalidator.IsUUID(did) {
40✔
1703
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
4✔
1704
                return
4✔
1705
        }
4✔
1706

1707
        page, perPage, err := rest_utils.ParsePagination(r)
32✔
1708
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
36✔
1709
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
4✔
1710
        }
4✔
1711
        if err != nil {
40✔
1712
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
8✔
1713
                return
8✔
1714
        }
8✔
1715

1716
        lq := store.ListQueryDeviceDeployments{
24✔
1717
                Skip:     int((page - 1) * perPage),
24✔
1718
                Limit:    int(perPage),
24✔
1719
                DeviceID: did,
24✔
1720
        }
24✔
1721
        if status := r.URL.Query().Get("status"); status != "" {
32✔
1722
                lq.Status = &status
8✔
1723
        }
8✔
1724
        if err = lq.Validate(); err != nil {
28✔
1725
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
1726
                return
4✔
1727
        }
4✔
1728

1729
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
20✔
1730
        if err != nil {
24✔
1731
                d.view.RenderInternalError(w, r, err, l)
4✔
1732
                return
4✔
1733
        }
4✔
1734
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
16✔
1735

16✔
1736
        hasNext := totalCount > lq.Skip+len(deps)
16✔
1737
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
16✔
1738
        for _, l := range links {
32✔
1739
                w.Header().Add("Link", l)
16✔
1740
        }
16✔
1741

1742
        d.view.RenderSuccessGet(w, deps)
16✔
1743
}
1744

1745
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
NEW
1746
        r *rest.Request) {
×
1747
        ctx := r.Context()
×
1748
        tenantID := r.PathParam("tenantID")
×
1749
        if tenantID != "" {
×
1750
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1751
                        Tenant:   tenantID,
×
1752
                        IsDevice: true,
×
1753
                })
×
1754
        }
×
1755

1756
        l := requestlog.GetRequestLogger(r)
×
1757

×
1758
        id := r.PathParam("id")
×
1759

×
1760
        // Decommission deployments for devices and update deployment stats
×
1761
        err := d.app.DecommissionDevice(ctx, id)
×
1762

×
1763
        switch err {
×
1764
        case nil, app.ErrStorageNotFound:
×
1765
                d.view.RenderEmptySuccessResponse(w)
×
1766
        default:
×
1767
                d.view.RenderInternalError(w, r, err, l)
×
1768

1769
        }
1770
}
1771

1772
// tenants
1773

1774
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1775
        ctx := r.Context()
1✔
1776
        l := requestlog.GetRequestLogger(r)
1✔
1777

1✔
1778
        defer r.Body.Close()
1✔
1779

1✔
1780
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1781
        if err != nil {
2✔
1782
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1783
                return
1✔
1784
        }
1✔
1785

1786
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1787
        if err != nil {
1✔
1788
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1789
                return
×
1790
        }
×
1791

1792
        w.WriteHeader(http.StatusCreated)
1✔
1793
}
1794

1795
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1796
        w rest.ResponseWriter,
1797
        r *rest.Request,
1798
) {
12✔
1799
        tenantID := r.PathParam("tenant")
12✔
1800
        if tenantID == "" {
14✔
1801
                l := requestlog.GetRequestLogger(r)
2✔
1802
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
2✔
1803
                return
2✔
1804
        }
2✔
1805

1806
        r.Request = r.WithContext(identity.WithContext(
10✔
1807
                r.Context(),
10✔
1808
                &identity.Identity{Tenant: tenantID},
10✔
1809
        ))
10✔
1810
        d.LookupDeployment(w, r)
10✔
1811
}
1812

1813
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1814
        w rest.ResponseWriter,
1815
        r *rest.Request,
1816
) {
9✔
1817
        l := requestlog.GetRequestLogger(r)
9✔
1818

9✔
1819
        tenantID := r.PathParam("tenant")
9✔
1820

9✔
1821
        ctx := identity.WithContext(
9✔
1822
                r.Context(),
9✔
1823
                &identity.Identity{Tenant: tenantID},
9✔
1824
        )
9✔
1825

9✔
1826
        settings, err := d.app.GetStorageSettings(ctx)
9✔
1827
        if err != nil {
13✔
1828
                rest_utils.RestErrWithLogInternal(w, r, l, err)
4✔
1829
                return
4✔
1830
        }
4✔
1831

1832
        d.view.RenderSuccessGet(w, settings)
5✔
1833
}
1834

1835
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1836
        w rest.ResponseWriter,
1837
        r *rest.Request,
1838
) {
19✔
1839
        l := requestlog.GetRequestLogger(r)
19✔
1840

19✔
1841
        defer r.Body.Close()
19✔
1842

19✔
1843
        tenantID := r.PathParam("tenant")
19✔
1844

19✔
1845
        ctx := identity.WithContext(
19✔
1846
                r.Context(),
19✔
1847
                &identity.Identity{Tenant: tenantID},
19✔
1848
        )
19✔
1849

19✔
1850
        settings, err := model.ParseStorageSettingsRequest(r.Body)
19✔
1851
        if err != nil {
24✔
1852
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
5✔
1853
                return
5✔
1854
        }
5✔
1855

1856
        err = d.app.SetStorageSettings(ctx, settings)
15✔
1857
        if err != nil {
19✔
1858
                rest_utils.RestErrWithLogInternal(w, r, l, err)
4✔
1859
                return
4✔
1860
        }
4✔
1861

1862
        w.WriteHeader(http.StatusNoContent)
11✔
1863
}
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