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

mendersoftware / deployments / 920043239

pending completion
920043239

Pull #872

gitlab-ci

alfrunes
chore: Restrict tag character set

Changelog: None
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #872: MEN-6348: Add tags to releases

220 of 229 new or added lines in 7 files covered. (96.07%)

223 existing lines in 7 files now uncovered.

7560 of 9480 relevant lines covered (79.75%)

34.07 hits per line

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

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

15
package http
16

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

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

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

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

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

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

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

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

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

86
const Redacted = "REDACTED"
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

281
        return filter
25✔
282
}
283

284
func redactReleaseName(r *rest.Request) {
17✔
285
        q := r.URL.Query()
17✔
286
        if q.Get(ParamName) != "" {
22✔
287
                q.Set(ParamName, Redacted)
5✔
288
                r.URL.RawQuery = q.Encode()
5✔
289
        }
5✔
290
}
291

292
func (d *DeploymentsApiHandlers) GetReleases(w rest.ResponseWriter, r *rest.Request) {
5✔
293
        l := requestlog.GetRequestLogger(r)
5✔
294

5✔
295
        defer redactReleaseName(r)
5✔
296
        filter := getReleaseOrImageFilter(r, false)
5✔
297
        releases, _, err := d.store.GetReleases(r.Context(), filter)
5✔
298
        if err != nil {
6✔
299
                d.view.RenderInternalError(w, r, err, l)
1✔
300
                return
1✔
301
        }
1✔
302

303
        d.view.RenderSuccessGet(w, releases)
4✔
304
}
305

306
func (d *DeploymentsApiHandlers) ListReleases(w rest.ResponseWriter, r *rest.Request) {
4✔
307
        l := requestlog.GetRequestLogger(r)
4✔
308

4✔
309
        defer redactReleaseName(r)
4✔
310
        filter := getReleaseOrImageFilter(r, true)
4✔
311
        releases, totalCount, err := d.store.GetReleases(r.Context(), filter)
4✔
312
        if err != nil {
5✔
313
                d.view.RenderInternalError(w, r, err, l)
1✔
314
                return
1✔
315
        }
1✔
316

317
        hasNext := totalCount > int(filter.Page*filter.PerPage)
3✔
318
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
3✔
319
        for _, l := range links {
6✔
320
                w.Header().Add("Link", l)
3✔
321
        }
3✔
322
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
323

3✔
324
        d.view.RenderSuccessGet(w, releases)
3✔
325
}
326

327
type limitResponse struct {
328
        Limit uint64 `json:"limit"`
329
        Usage uint64 `json:"usage"`
330
}
331

332
func (d *DeploymentsApiHandlers) GetLimit(w rest.ResponseWriter, r *rest.Request) {
3✔
333
        l := requestlog.GetRequestLogger(r)
3✔
334

3✔
335
        name := r.PathParam("name")
3✔
336

3✔
337
        if !model.IsValidLimit(name) {
4✔
338
                d.view.RenderError(w, r,
1✔
339
                        errors.Errorf("unsupported limit %s", name),
1✔
340
                        http.StatusBadRequest, l)
1✔
341
                return
1✔
342
        }
1✔
343

344
        limit, err := d.app.GetLimit(r.Context(), name)
2✔
345
        if err != nil {
3✔
346
                d.view.RenderInternalError(w, r, err, l)
1✔
347
                return
1✔
348
        }
1✔
349

350
        d.view.RenderSuccessGet(w, limitResponse{
1✔
351
                Limit: limit.Value,
1✔
352
                Usage: 0, // TODO fill this when ready
1✔
353
        })
1✔
354
}
355

356
// images
357

358
func (d *DeploymentsApiHandlers) GetImage(w rest.ResponseWriter, r *rest.Request) {
1✔
359
        l := requestlog.GetRequestLogger(r)
1✔
360

1✔
361
        id := r.PathParam("id")
1✔
362

1✔
363
        if !govalidator.IsUUID(id) {
2✔
364
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
365
                return
1✔
366
        }
1✔
367

368
        image, err := d.app.GetImage(r.Context(), id)
1✔
369
        if err != nil {
1✔
370
                d.view.RenderInternalError(w, r, err, l)
×
371
                return
×
372
        }
×
373

374
        if image == nil {
2✔
375
                d.view.RenderErrorNotFound(w, r, l)
1✔
376
                return
1✔
377
        }
1✔
378

379
        d.view.RenderSuccessGet(w, image)
1✔
380
}
381

382
func (d *DeploymentsApiHandlers) GetImages(w rest.ResponseWriter, r *rest.Request) {
5✔
383
        l := requestlog.GetRequestLogger(r)
5✔
384

5✔
385
        defer redactReleaseName(r)
5✔
386
        filter := getReleaseOrImageFilter(r, false)
5✔
387

5✔
388
        list, _, err := d.app.ListImages(r.Context(), filter)
5✔
389
        if err != nil {
6✔
390
                d.view.RenderInternalError(w, r, err, l)
1✔
391
                return
1✔
392
        }
1✔
393

394
        d.view.RenderSuccessGet(w, list)
4✔
395
}
396

397
func (d *DeploymentsApiHandlers) ListImages(w rest.ResponseWriter, r *rest.Request) {
4✔
398
        l := requestlog.GetRequestLogger(r)
4✔
399

4✔
400
        defer redactReleaseName(r)
4✔
401
        filter := getReleaseOrImageFilter(r, true)
4✔
402

4✔
403
        list, totalCount, err := d.app.ListImages(r.Context(), filter)
4✔
404
        if err != nil {
5✔
405
                d.view.RenderInternalError(w, r, err, l)
1✔
406
                return
1✔
407
        }
1✔
408

409
        hasNext := totalCount > int(filter.Page*filter.PerPage)
3✔
410
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
3✔
411
        for _, l := range links {
6✔
412
                w.Header().Add("Link", l)
3✔
413
        }
3✔
414
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
415

3✔
416
        d.view.RenderSuccessGet(w, list)
3✔
417
}
418

419
func (d *DeploymentsApiHandlers) DownloadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
420
        l := requestlog.GetRequestLogger(r)
1✔
421

1✔
422
        id := r.PathParam("id")
1✔
423

1✔
424
        if !govalidator.IsUUID(id) {
1✔
425
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
426
                return
×
427
        }
×
428

429
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
430
        link, err := d.app.DownloadLink(r.Context(), id, time.Duration(expireSeconds)*time.Second)
1✔
431
        if err != nil {
1✔
432
                d.view.RenderInternalError(w, r, err, l)
×
433
                return
×
434
        }
×
435

436
        if link == nil {
1✔
437
                d.view.RenderErrorNotFound(w, r, l)
×
438
                return
×
439
        }
×
440

441
        d.view.RenderSuccessGet(w, link)
1✔
442
}
443

444
func (d *DeploymentsApiHandlers) UploadLink(w rest.ResponseWriter, r *rest.Request) {
4✔
445
        l := requestlog.GetRequestLogger(r)
4✔
446

4✔
447
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
4✔
448
        link, err := d.app.UploadLink(
4✔
449
                r.Context(),
4✔
450
                time.Duration(expireSeconds)*time.Second,
4✔
451
                d.config.EnableDirectUploadSkipVerify,
4✔
452
        )
4✔
453
        if err != nil {
5✔
454
                d.view.RenderInternalError(w, r, err, l)
1✔
455
                return
1✔
456
        }
1✔
457

458
        if link == nil {
4✔
459
                d.view.RenderErrorNotFound(w, r, l)
1✔
460
                return
1✔
461
        }
1✔
462

463
        d.view.RenderSuccessGet(w, link)
2✔
464
}
465

466
func (d *DeploymentsApiHandlers) CompleteUpload(w rest.ResponseWriter, r *rest.Request) {
4✔
467
        ctx := r.Context()
4✔
468
        l := log.FromContext(ctx)
4✔
469

4✔
470
        artifactID := r.PathParam(ParamID)
4✔
471

4✔
472
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify)
4✔
473
        switch errors.Cause(err) {
4✔
474
        case nil:
2✔
475
                // w.Header().Set("Link", "FEAT: Upload status API")
2✔
476
                w.WriteHeader(http.StatusAccepted)
2✔
477
        case app.ErrUploadNotFound:
1✔
478
                d.view.RenderErrorNotFound(w, r, l)
1✔
479
        default:
1✔
480
                l.Error(err)
1✔
481
                w.WriteHeader(http.StatusInternalServerError)
1✔
482
                w.WriteJson(rest_utils.ApiError{ // nolint:errcheck
1✔
483
                        Err:   "internal server error",
1✔
484
                        ReqId: requestid.FromContext(ctx),
1✔
485
                })
1✔
486
        }
487
}
488

489
func (d *DeploymentsApiHandlers) DownloadConfiguration(w rest.ResponseWriter, r *rest.Request) {
10✔
490
        if d.config.PresignSecret == nil {
11✔
491
                rest.NotFound(w, r)
1✔
492
                return
1✔
493
        }
1✔
494
        var (
9✔
495
                deviceID, _     = url.PathUnescape(r.PathParam(ParamDeviceID))
9✔
496
                deviceType, _   = url.PathUnescape(r.PathParam(ParamDeviceType))
9✔
497
                deploymentID, _ = url.PathUnescape(r.PathParam(ParamDeploymentID))
9✔
498
        )
9✔
499
        if deviceID == "" || deviceType == "" || deploymentID == "" {
9✔
500
                rest.NotFound(w, r)
×
501
                return
×
502
        }
×
503

504
        var (
9✔
505
                tenantID string
9✔
506
                l        = log.FromContext(r.Context())
9✔
507
                q        = r.URL.Query()
9✔
508
                err      error
9✔
509
        )
9✔
510
        tenantID = q.Get(ParamTenantID)
9✔
511
        sig := model.NewRequestSignature(r.Request, d.config.PresignSecret)
9✔
512
        if err = sig.Validate(); err != nil {
12✔
513
                switch cause := errors.Cause(err); cause {
3✔
514
                case model.ErrLinkExpired:
1✔
515
                        d.view.RenderError(w, r, cause, http.StatusForbidden, l)
1✔
516
                default:
2✔
517
                        d.view.RenderError(w, r,
2✔
518
                                errors.Wrap(err, "invalid request parameters"),
2✔
519
                                http.StatusBadRequest, l,
2✔
520
                        )
2✔
521
                }
522
                return
3✔
523
        }
524

525
        if !sig.VerifyHMAC256() {
9✔
526
                d.view.RenderError(w, r,
2✔
527
                        errors.New("signature invalid"),
2✔
528
                        http.StatusForbidden, l,
2✔
529
                )
2✔
530
                return
2✔
531
        }
2✔
532

533
        // Validate request signature
534
        ctx := identity.WithContext(r.Context(), &identity.Identity{
6✔
535
                Subject:  deviceID,
6✔
536
                Tenant:   tenantID,
6✔
537
                IsDevice: true,
6✔
538
        })
6✔
539

6✔
540
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
6✔
541
        if err != nil {
8✔
542
                switch cause := errors.Cause(err); cause {
2✔
543
                case app.ErrModelDeploymentNotFound:
1✔
544
                        d.view.RenderError(w, r,
1✔
545
                                errors.Errorf(
1✔
546
                                        "deployment with id '%s' not found",
1✔
547
                                        deploymentID,
1✔
548
                                ),
1✔
549
                                http.StatusNotFound, l,
1✔
550
                        )
1✔
551
                default:
1✔
552
                        l.Error(err.Error())
1✔
553
                        d.view.RenderInternalError(w, r, err, l)
1✔
554
                }
555
                return
2✔
556
        }
557
        artifactPayload, err := io.ReadAll(artifact)
4✔
558
        if err != nil {
5✔
559
                l.Error(err.Error())
1✔
560
                d.view.RenderInternalError(w, r, err, l)
1✔
561
                return
1✔
562
        }
1✔
563

564
        rw := w.(http.ResponseWriter)
3✔
565
        hdr := rw.Header()
3✔
566
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
3✔
567
        hdr.Set("Content-Type", app.ArtifactContentType)
3✔
568
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
3✔
569
        rw.WriteHeader(http.StatusOK)
3✔
570
        _, err = rw.Write(artifactPayload)
3✔
571
        if err != nil {
3✔
572
                // There's not anything we can do here in terms of the response.
×
573
                l.Error(err.Error())
×
574
        }
×
575
}
576

577
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
1✔
578
        l := requestlog.GetRequestLogger(r)
1✔
579

1✔
580
        id := r.PathParam("id")
1✔
581

1✔
582
        if !govalidator.IsUUID(id) {
1✔
583
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
584
                return
×
585
        }
×
586

587
        if err := d.app.DeleteImage(r.Context(), id); err != nil {
2✔
588
                switch err {
1✔
589
                default:
×
590
                        d.view.RenderInternalError(w, r, err, l)
×
591
                case app.ErrImageMetaNotFound:
×
592
                        d.view.RenderErrorNotFound(w, r, l)
×
593
                case app.ErrModelImageInActiveDeployment:
1✔
594
                        d.view.RenderError(w, r, ErrArtifactUsedInActiveDeployment, http.StatusConflict, l)
1✔
595
                }
596
                return
1✔
597
        }
598

599
        d.view.RenderSuccessDelete(w)
1✔
600
}
601

602
func (d *DeploymentsApiHandlers) EditImage(w rest.ResponseWriter, r *rest.Request) {
×
603
        l := requestlog.GetRequestLogger(r)
×
604

×
605
        id := r.PathParam("id")
×
606

×
607
        if !govalidator.IsUUID(id) {
×
608
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
609
                return
×
610
        }
×
611

612
        constructor, err := getImageMetaFromBody(r)
×
613
        if err != nil {
×
614
                d.view.RenderError(
×
615
                        w,
×
616
                        r,
×
617
                        errors.Wrap(err, "Validating request body"),
×
618
                        http.StatusBadRequest,
×
619
                        l,
×
620
                )
×
621
                return
×
622
        }
×
623

624
        found, err := d.app.EditImage(r.Context(), id, constructor)
×
625
        if err != nil {
×
626
                if err == app.ErrModelImageUsedInAnyDeployment {
×
627
                        d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
628
                        return
×
629
                }
×
630
                d.view.RenderInternalError(w, r, err, l)
×
631
                return
×
632
        }
633

634
        if !found {
×
635
                d.view.RenderErrorNotFound(w, r, l)
×
636
                return
×
637
        }
×
638

639
        d.view.RenderSuccessPut(w)
×
640
}
641

642
func getImageMetaFromBody(r *rest.Request) (*model.ImageMeta, error) {
×
643

×
644
        var constructor *model.ImageMeta
×
645

×
646
        if err := r.DecodeJsonPayload(&constructor); err != nil {
×
647
                return nil, err
×
648
        }
×
649

650
        if err := constructor.Validate(); err != nil {
×
651
                return nil, err
×
652
        }
×
653

654
        return constructor, nil
×
655
}
656

657
// NewImage is the Multipart Image/Meta upload handler.
658
// Request should be of type "multipart/form-data". The parts are
659
// key/valyue pairs of metadata information except the last one,
660
// which must contain the artifact file.
661
func (d *DeploymentsApiHandlers) NewImage(w rest.ResponseWriter, r *rest.Request) {
7✔
662
        d.newImageWithContext(r.Context(), w, r)
7✔
663
}
7✔
664

665
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
666
        l := requestlog.GetRequestLogger(r)
7✔
667

7✔
668
        tenantID := r.PathParam("tenant")
7✔
669

7✔
670
        if tenantID == "" {
7✔
671
                rest_utils.RestErrWithLog(
×
672
                        w,
×
673
                        r,
×
674
                        l,
×
675
                        fmt.Errorf("missing tenant id in path"),
×
676
                        http.StatusBadRequest,
×
677
                )
×
678
                return
×
679
        }
×
680

681
        var ctx context.Context
7✔
682
        if tenantID != "default" {
8✔
683
                ident := &identity.Identity{Tenant: tenantID}
1✔
684
                ctx = identity.WithContext(r.Context(), ident)
1✔
685
        } else {
7✔
686
                ctx = r.Context()
6✔
687
        }
6✔
688

689
        d.newImageWithContext(ctx, w, r)
7✔
690
}
691

692
func (d *DeploymentsApiHandlers) newImageWithContext(
693
        ctx context.Context,
694
        w rest.ResponseWriter,
695
        r *rest.Request,
696
) {
13✔
697
        l := requestlog.GetRequestLogger(r)
13✔
698

13✔
699
        formReader, err := r.MultipartReader()
13✔
700
        if err != nil {
17✔
701
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
702
                return
4✔
703
        }
4✔
704

705
        // parse multipart message
706
        multipartUploadMsg, err := d.ParseMultipart(formReader)
9✔
707

9✔
708
        if err != nil {
14✔
709
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
5✔
710
                return
5✔
711
        }
5✔
712

713
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
5✔
714
        if err == nil {
8✔
715
                d.view.RenderSuccessPost(w, r, imgID)
3✔
716
                return
3✔
717
        }
3✔
718
        if cErr, ok := err.(*model.ConflictError); ok {
5✔
719
                d.view.RenderError(w, r, cErr, http.StatusConflict, l)
2✔
720
                return
2✔
721
        }
2✔
722
        cause := errors.Cause(err)
1✔
723
        switch cause {
1✔
724
        default:
×
725
                d.view.RenderInternalError(w, r, err, l)
×
726
                return
×
727
        case app.ErrModelArtifactNotUnique:
×
728
                l.Error(err.Error())
×
729
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
730
                return
×
731
        case app.ErrModelParsingArtifactFailed:
1✔
732
                l.Error(err.Error())
1✔
733
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
734
                return
1✔
735
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
736
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
737
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
738
                l.Error(err.Error())
×
739
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
740
                return
×
741
        }
742
}
743

744
func formatArtifactUploadError(err error) error {
2✔
745
        // remove generic message
2✔
746
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
747

2✔
748
        // handle specific cases
2✔
749

2✔
750
        if strings.Contains(errMsg, "invalid checksum") {
2✔
751
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
752
        }
×
753

754
        if strings.Contains(errMsg, "unsupported version") {
2✔
755
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
756
                        "; supported versions are: 1, 2")
×
757
        }
×
758

759
        return errors.New(errMsg)
2✔
760
}
761

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

13✔
770
        formReader, err := r.MultipartReader()
13✔
771
        if err != nil {
15✔
772
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
773
                return
2✔
774
        }
2✔
775

776
        // parse multipart message
777
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
11✔
778
        if err != nil {
15✔
779
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
780
                return
4✔
781
        }
4✔
782

783
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
7✔
784
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
14✔
785
                multipartMsg.Token = tokenFields[1]
7✔
786
        }
7✔
787

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

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

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

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

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

881
                default:
4✔
882
                        // Ignore all non-API sections.
4✔
883
                        continue
4✔
884
                }
885
        }
886
}
887

888
// ParseGenerateImageMultipart parses multipart/form-data message.
889
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
890
        r *multipart.Reader,
891
) (*model.MultipartGenerateImageMsg, error) {
11✔
892
        msg := &model.MultipartGenerateImageMsg{}
11✔
893
        var size int64
11✔
894

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

914
                case "description":
7✔
915
                        b, err := io.ReadAll(part)
7✔
916
                        if err != nil {
7✔
917
                                return nil, errors.Wrap(err,
×
918
                                        "failed to read form value 'description'",
×
919
                                )
×
920
                        }
×
921
                        msg.Description = string(b)
7✔
922

923
                case "device_types_compatible":
9✔
924
                        b, err := io.ReadAll(part)
9✔
925
                        if err != nil {
9✔
926
                                return nil, errors.Wrap(err,
×
927
                                        "failed to read form value 'device_types_compatible'",
×
928
                                )
×
929
                        }
×
930
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
9✔
931

932
                case "file":
9✔
933
                        if size > 0 {
15✔
934
                                msg.FileReader = utils.ReadExactly(part, size)
6✔
935
                        } else {
9✔
936
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxImageSize)
3✔
937
                        }
3✔
938
                        break ParseLoop
9✔
939

940
                case "name":
10✔
941
                        b, err := io.ReadAll(part)
10✔
942
                        if err != nil {
10✔
943
                                return nil, errors.Wrap(err,
×
944
                                        "failed to read form value 'name'",
×
945
                                )
×
946
                        }
×
947
                        msg.Name = string(b)
10✔
948

949
                case "type":
9✔
950
                        b, err := io.ReadAll(part)
9✔
951
                        if err != nil {
9✔
952
                                return nil, errors.Wrap(err,
×
953
                                        "failed to read form value 'type'",
×
954
                                )
×
955
                        }
×
956
                        msg.Type = string(b)
9✔
957

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

974
                default:
×
975
                        // Ignore non-API sections.
×
976
                        continue
×
977
                }
978
        }
979

980
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
11✔
981
}
982

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

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

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

8✔
1023
        d.createDeployment(w, r, ctx, l, "")
8✔
1024
}
8✔
1025

1026
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
1027
        ctx := r.Context()
5✔
1028
        l := requestlog.GetRequestLogger(r)
5✔
1029

5✔
1030
        group := r.PathParam("name")
5✔
1031
        if len(group) < 1 {
5✔
1032
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
1033
        }
×
1034
        d.createDeployment(w, r, ctx, l, group)
5✔
1035
}
1036

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

1052
// getConfigurationDeploymentConstructorFromBody extracts configuration
1053
// deployment constructor from the request body and validates it
1054
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
1055
        *model.ConfigurationDeploymentConstructor, error) {
7✔
1056

7✔
1057
        var constructor *model.ConfigurationDeploymentConstructor
7✔
1058

7✔
1059
        if err := r.DecodeJsonPayload(&constructor); err != nil {
8✔
1060
                return nil, err
1✔
1061
        }
1✔
1062

1063
        if err := constructor.Validate(); err != nil {
8✔
1064
                return nil, err
2✔
1065
        }
2✔
1066

1067
        return constructor, nil
5✔
1068
}
1069

1070
// device configuration deployment handler
1071
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1072
        w rest.ResponseWriter,
1073
        r *rest.Request,
1074
) {
7✔
1075
        l := requestlog.GetRequestLogger(r)
7✔
1076

7✔
1077
        // get path params
7✔
1078
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
7✔
1079
        if err != nil {
7✔
1080
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
1081
                return
×
1082
        }
×
1083

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

7✔
1087
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
7✔
1088
        if err != nil {
10✔
1089
                d.view.RenderError(
3✔
1090
                        w,
3✔
1091
                        r,
3✔
1092
                        errors.Wrap(err, "Validating request body"),
3✔
1093
                        http.StatusBadRequest,
3✔
1094
                        l,
3✔
1095
                )
3✔
1096
                return
3✔
1097
        }
3✔
1098

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

1113
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1114
        r *rest.Request,
1115
        group string,
1116
) (*model.DeploymentConstructor, error) {
13✔
1117
        var constructor *model.DeploymentConstructor
13✔
1118
        if err := r.DecodeJsonPayload(&constructor); err != nil {
16✔
1119
                return nil, err
3✔
1120
        }
3✔
1121

1122
        constructor.Group = group
11✔
1123

11✔
1124
        if err := constructor.ValidateNew(); err != nil {
15✔
1125
                return nil, err
4✔
1126
        }
4✔
1127

1128
        return constructor, nil
8✔
1129
}
1130

1131
func (d *DeploymentsApiHandlers) GetDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1132
        ctx := r.Context()
1✔
1133
        l := requestlog.GetRequestLogger(r)
1✔
1134

1✔
1135
        id := r.PathParam("id")
1✔
1136

1✔
1137
        if !govalidator.IsUUID(id) {
2✔
1138
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
1139
                return
1✔
1140
        }
1✔
1141

1142
        deployment, err := d.app.GetDeployment(ctx, id)
1✔
1143
        if err != nil {
1✔
1144
                d.view.RenderInternalError(w, r, err, l)
×
1145
                return
×
1146
        }
×
1147

1148
        if deployment == nil {
1✔
1149
                d.view.RenderErrorNotFound(w, r, l)
×
1150
                return
×
1151
        }
×
1152

1153
        d.view.RenderSuccessGet(w, deployment)
1✔
1154
}
1155

1156
func (d *DeploymentsApiHandlers) GetDeploymentStats(w rest.ResponseWriter, r *rest.Request) {
1✔
1157
        ctx := r.Context()
1✔
1158
        l := requestlog.GetRequestLogger(r)
1✔
1159

1✔
1160
        id := r.PathParam("id")
1✔
1161

1✔
1162
        if !govalidator.IsUUID(id) {
1✔
1163
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1164
                return
×
1165
        }
×
1166

1167
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1168
        if err != nil {
1✔
1169
                d.view.RenderInternalError(w, r, err, l)
×
1170
                return
×
1171
        }
×
1172

1173
        if stats == nil {
1✔
1174
                d.view.RenderErrorNotFound(w, r, l)
×
1175
                return
×
1176
        }
×
1177

1178
        d.view.RenderSuccessGet(w, stats)
1✔
1179
}
1180

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

4✔
1183
        ctx := r.Context()
4✔
1184
        l := requestlog.GetRequestLogger(r)
4✔
1185

4✔
1186
        ids := model.DeploymentIDs{}
4✔
1187
        if err := r.DecodeJsonPayload(&ids); err != nil {
4✔
1188
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1189
                return
×
1190
        }
×
1191

1192
        if len(ids.IDs) == 0 {
4✔
1193
                w.WriteHeader(http.StatusOK)
×
1194
                _ = w.WriteJson(struct{}{})
×
1195
                return
×
1196
        }
×
1197

1198
        if err := ids.Validate(); err != nil {
5✔
1199
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1200
                return
1✔
1201
        }
1✔
1202

1203
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
3✔
1204
        if err != nil {
5✔
1205
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
3✔
1206
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
1✔
1207
                        return
1✔
1208
                }
1✔
1209
                d.view.RenderInternalError(w, r, err, l)
1✔
1210
                return
1✔
1211
        }
1212

1213
        w.WriteHeader(http.StatusOK)
1✔
1214

1✔
1215
        _ = w.WriteJson(stats)
1✔
1216
}
1217

1218
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
1219
        ctx := r.Context()
×
1220
        l := requestlog.GetRequestLogger(r)
×
1221

×
1222
        id := r.PathParam("id")
×
1223

×
1224
        if !govalidator.IsUUID(id) {
×
1225
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1226
                return
×
1227
        }
×
1228

1229
        deployment, err := d.app.GetDeployment(ctx, id)
×
1230
        if err != nil {
×
1231
                d.view.RenderInternalError(w, r, err, l)
×
1232
                return
×
1233
        }
×
1234

1235
        if deployment == nil {
×
1236
                d.view.RenderErrorNotFound(w, r, l)
×
1237
                return
×
1238
        }
×
1239

1240
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1241
}
1242

1243
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1244
        ctx := r.Context()
1✔
1245
        l := requestlog.GetRequestLogger(r)
1✔
1246

1✔
1247
        id := r.PathParam("id")
1✔
1248

1✔
1249
        if !govalidator.IsUUID(id) {
1✔
1250
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1251
                return
×
1252
        }
×
1253

1254
        // receive request body
1255
        var status struct {
1✔
1256
                Status model.DeviceDeploymentStatus
1✔
1257
        }
1✔
1258

1✔
1259
        err := r.DecodeJsonPayload(&status)
1✔
1260
        if err != nil {
1✔
1261
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1262
                return
×
1263
        }
×
1264
        // "aborted" is the only supported status
1265
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1266
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
1267
        }
×
1268

1269
        l.Infof("Abort deployment: %s", id)
1✔
1270

1✔
1271
        // Check if deployment is finished
1✔
1272
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1273
        if err != nil {
1✔
1274
                d.view.RenderInternalError(w, r, err, l)
×
1275
                return
×
1276
        }
×
1277
        if isDeploymentFinished {
2✔
1278
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
1✔
1279
                return
1✔
1280
        }
1✔
1281

1282
        // Abort deployments for devices and update deployment stats
1283
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1284
                d.view.RenderInternalError(w, r, err, l)
×
1285
        }
×
1286

1287
        d.view.RenderEmptySuccessResponse(w)
1✔
1288
}
1289

1290
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
12✔
1291
        var (
12✔
1292
                installed *model.InstalledDeviceDeployment
12✔
1293
                ctx       = r.Context()
12✔
1294
                l         = requestlog.GetRequestLogger(r)
12✔
1295
                idata     = identity.FromContext(ctx)
12✔
1296
        )
12✔
1297
        if idata == nil {
14✔
1298
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
2✔
1299
                return
2✔
1300
        }
2✔
1301

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

1334
        if err := installed.Validate(); err != nil {
11✔
1335
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1336
                return
1✔
1337
        }
1✔
1338

1339
        request := &model.DeploymentNextRequest{
9✔
1340
                DeviceProvides: installed,
9✔
1341
        }
9✔
1342

9✔
1343
        d.getDeploymentForDevice(w, r, idata, request)
9✔
1344
}
1345

1346
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1347
        w rest.ResponseWriter,
1348
        r *rest.Request,
1349
        idata *identity.Identity,
1350
        request *model.DeploymentNextRequest,
1351
) {
9✔
1352
        ctx := r.Context()
9✔
1353
        l := requestlog.GetRequestLogger(r)
9✔
1354

9✔
1355
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
9✔
1356
        if err != nil {
11✔
1357
                if err == app.ErrConflictingRequestData {
3✔
1358
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1359
                } else {
2✔
1360
                        d.view.RenderInternalError(w, r, err, l)
1✔
1361
                }
1✔
1362
                return
2✔
1363
        }
1364

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

1403
        d.view.RenderSuccessGet(w, deployment)
6✔
1404
}
1405

1406
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
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
        idata := identity.FromContext(ctx)
1✔
1416
        if idata == nil {
1✔
1417
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1418
                return
×
1419
        }
×
1420

1421
        // receive request body
1422
        var report model.StatusReport
1✔
1423

1✔
1424
        err := r.DecodeJsonPayload(&report)
1✔
1425
        if err != nil {
1✔
1426
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1427
                return
×
1428
        }
×
1429

1430
        l.Infof("status: %+v", report)
1✔
1431
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
1✔
1432
                idata.Subject, model.DeviceDeploymentState{
1✔
1433
                        Status:   report.Status,
1✔
1434
                        SubState: report.SubState,
1✔
1435
                }); err != nil {
1✔
1436

×
1437
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
1438
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
1439
                } else if err == app.ErrStorageNotFound {
×
1440
                        d.view.RenderErrorNotFound(w, r, l)
×
1441
                } else {
×
1442
                        d.view.RenderInternalError(w, r, err, l)
×
1443
                }
×
1444
                return
×
1445
        }
1446

1447
        d.view.RenderEmptySuccessResponse(w)
1✔
1448
}
1449

1450
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1451
        w rest.ResponseWriter,
1452
        r *rest.Request,
1453
) {
1✔
1454
        ctx := r.Context()
1✔
1455
        l := requestlog.GetRequestLogger(r)
1✔
1456

1✔
1457
        did := r.PathParam("id")
1✔
1458

1✔
1459
        if !govalidator.IsUUID(did) {
1✔
1460
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1461
                return
×
1462
        }
×
1463

1464
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
1✔
1465
        if err != nil {
1✔
1466
                switch err {
×
1467
                case app.ErrModelDeploymentNotFound:
×
1468
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1469
                        return
×
1470
                default:
×
1471
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1472
                        return
×
1473
                }
1474
        }
1475

1476
        d.view.RenderSuccessGet(w, statuses)
1✔
1477
}
1478

1479
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1480
        w rest.ResponseWriter,
1481
        r *rest.Request,
1482
) {
1✔
1483
        ctx := r.Context()
1✔
1484
        l := requestlog.GetRequestLogger(r)
1✔
1485

1✔
1486
        did := r.PathParam("id")
1✔
1487

1✔
1488
        if !govalidator.IsUUID(did) {
1✔
1489
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1490
                return
×
1491
        }
×
1492

1493
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1494
        if err != nil {
1✔
1495
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1496
                return
×
1497
        }
×
1498

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

1512
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1513
        if err != nil {
1✔
1514
                switch err {
×
1515
                case app.ErrModelDeploymentNotFound:
×
1516
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1517
                        return
×
1518
                default:
×
1519
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1520
                        return
×
1521
                }
1522
        }
1523

1524
        hasNext := totalCount > int(page*perPage)
1✔
1525
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1526
        for _, l := range links {
2✔
1527
                w.Header().Add("Link", l)
1✔
1528
        }
1✔
1529
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1530
        d.view.RenderSuccessGet(w, statuses)
1✔
1531
}
1532

1533
func ParseLookupQuery(vals url.Values) (model.Query, error) {
9✔
1534
        query := model.Query{}
9✔
1535

9✔
1536
        search := vals.Get("search")
9✔
1537
        if search != "" {
9✔
1538
                query.SearchText = search
×
1539
        }
×
1540

1541
        createdBefore := vals.Get("created_before")
9✔
1542
        if createdBefore != "" {
10✔
1543
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
2✔
1544
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1545
                } else {
1✔
1546
                        query.CreatedBefore = &createdBeforeTime
×
1547
                }
×
1548
        }
1549

1550
        createdAfter := vals.Get("created_after")
8✔
1551
        if createdAfter != "" {
8✔
1552
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1553
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1554
                } else {
×
1555
                        query.CreatedAfter = &createdAfterTime
×
1556
                }
×
1557
        }
1558

1559
        switch strings.ToLower(vals.Get("sort")) {
8✔
1560
        case model.SortDirectionAscending:
1✔
1561
                query.Sort = model.SortDirectionAscending
1✔
1562
        case "", model.SortDirectionDescending:
7✔
1563
                query.Sort = model.SortDirectionDescending
7✔
1564
        default:
×
1565
                return query, ErrInvalidSortDirection
×
1566
        }
1567

1568
        status := vals.Get("status")
8✔
1569
        switch status {
8✔
1570
        case "inprogress":
×
1571
                query.Status = model.StatusQueryInProgress
×
1572
        case "finished":
×
1573
                query.Status = model.StatusQueryFinished
×
1574
        case "pending":
×
1575
                query.Status = model.StatusQueryPending
×
1576
        case "aborted":
×
1577
                query.Status = model.StatusQueryAborted
×
1578
        case "":
8✔
1579
                query.Status = model.StatusQueryAny
8✔
1580
        default:
×
1581
                return query, errors.Errorf("unknown status %s", status)
×
1582

1583
        }
1584

1585
        dType := vals.Get("type")
8✔
1586
        if dType == "" {
16✔
1587
                return query, nil
8✔
1588
        }
8✔
1589
        deploymentType := model.DeploymentType(dType)
×
1590
        if deploymentType == model.DeploymentTypeSoftware ||
×
1591
                deploymentType == model.DeploymentTypeConfiguration {
×
1592
                query.Type = deploymentType
×
1593
        } else {
×
1594
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1595
        }
×
1596

1597
        return query, nil
×
1598
}
1599

1600
func parseEpochToTimestamp(epoch string) (time.Time, error) {
1✔
1601
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
2✔
1602
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
1✔
1603
        } else {
1✔
1604
                return time.Unix(epochInt64, 0).UTC(), nil
×
1605
        }
×
1606
}
1607

1608
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
9✔
1609
        ctx := r.Context()
9✔
1610
        l := requestlog.GetRequestLogger(r)
9✔
1611
        q := r.URL.Query()
9✔
1612
        defer func() {
18✔
1613
                if search := q.Get("search"); search != "" {
9✔
1614
                        q.Set("search", Redacted)
×
1615
                        r.URL.RawQuery = q.Encode()
×
1616
                }
×
1617
        }()
1618

1619
        query, err := ParseLookupQuery(q)
9✔
1620
        if err != nil {
10✔
1621
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1622
                return
1✔
1623
        }
1✔
1624

1625
        page, perPage, err := rest_utils.ParsePagination(r)
8✔
1626
        if err != nil {
9✔
1627
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1628
                return
1✔
1629
        }
1✔
1630
        query.Skip = int((page - 1) * perPage)
7✔
1631
        query.Limit = int(perPage + 1)
7✔
1632

7✔
1633
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
7✔
1634
        if err != nil {
8✔
1635
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1636
                return
1✔
1637
        }
1✔
1638
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
6✔
1639

6✔
1640
        len := len(deps)
6✔
1641
        hasNext := false
6✔
1642
        if uint64(len) > perPage {
6✔
1643
                hasNext = true
×
1644
                len = int(perPage)
×
1645
        }
×
1646

1647
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
6✔
1648
        for _, l := range links {
13✔
1649
                w.Header().Add("Link", l)
7✔
1650
        }
7✔
1651

1652
        d.view.RenderSuccessGet(w, deps[:len])
6✔
1653
}
1654

1655
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1656
        ctx := r.Context()
1✔
1657
        l := requestlog.GetRequestLogger(r)
1✔
1658

1✔
1659
        did := r.PathParam("id")
1✔
1660

1✔
1661
        idata := identity.FromContext(ctx)
1✔
1662
        if idata == nil {
1✔
1663
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1664
                return
×
1665
        }
×
1666

1667
        // reuse DeploymentLog, device and deployment IDs are ignored when
1668
        // (un-)marshaling DeploymentLog to/from JSON
1669
        var log model.DeploymentLog
1✔
1670

1✔
1671
        err := r.DecodeJsonPayload(&log)
1✔
1672
        if err != nil {
1✔
1673
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1674
                return
×
1675
        }
×
1676

1677
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1678
                did, log.Messages); err != nil {
1✔
1679

×
1680
                if err == app.ErrModelDeploymentNotFound {
×
1681
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1682
                } else {
×
1683
                        d.view.RenderInternalError(w, r, err, l)
×
1684
                }
×
1685
                return
×
1686
        }
1687

1688
        d.view.RenderEmptySuccessResponse(w)
1✔
1689
}
1690

1691
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1692
        ctx := r.Context()
1✔
1693
        l := requestlog.GetRequestLogger(r)
1✔
1694

1✔
1695
        did := r.PathParam("id")
1✔
1696
        devid := r.PathParam("devid")
1✔
1697

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

1✔
1700
        if err != nil {
1✔
1701
                d.view.RenderInternalError(w, r, err, l)
×
1702
                return
×
1703
        }
×
1704

1705
        if depl == nil {
1✔
1706
                d.view.RenderErrorNotFound(w, r, l)
×
1707
                return
×
1708
        }
×
1709

1710
        d.view.RenderDeploymentLog(w, *depl)
1✔
1711
}
1712

1713
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
3✔
1714
        ctx := r.Context()
3✔
1715
        l := requestlog.GetRequestLogger(r)
3✔
1716

3✔
1717
        id := r.PathParam("id")
3✔
1718
        err := d.app.AbortDeviceDeployments(ctx, id)
3✔
1719

3✔
1720
        switch err {
3✔
1721
        case nil, app.ErrStorageNotFound:
2✔
1722
                d.view.RenderEmptySuccessResponse(w)
2✔
1723
        default:
1✔
1724
                d.view.RenderInternalError(w, r, err, l)
1✔
1725
        }
1726
}
1727

1728
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1729
        r *rest.Request) {
3✔
1730
        ctx := r.Context()
3✔
1731
        l := requestlog.GetRequestLogger(r)
3✔
1732

3✔
1733
        id := r.PathParam("id")
3✔
1734
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
3✔
1735

3✔
1736
        switch err {
3✔
1737
        case nil, app.ErrStorageNotFound:
2✔
1738
                d.view.RenderEmptySuccessResponse(w)
2✔
1739
        default:
1✔
1740
                d.view.RenderInternalError(w, r, err, l)
1✔
1741
        }
1742
}
1743

1744
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
9✔
1745
        ctx := r.Context()
9✔
1746
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1747
}
9✔
1748

1749
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1750
        r *rest.Request) {
9✔
1751
        ctx := r.Context()
9✔
1752
        tenantID := r.PathParam("tenant")
9✔
1753
        if tenantID != "" {
18✔
1754
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1755
                        Tenant:   tenantID,
9✔
1756
                        IsDevice: true,
9✔
1757
                })
9✔
1758
        }
9✔
1759
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1760
}
1761

1762
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(w rest.ResponseWriter,
1763
        r *rest.Request) {
9✔
1764
        ctx := r.Context()
9✔
1765
        tenantID := r.PathParam("tenant")
9✔
1766
        if tenantID != "" {
18✔
1767
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1768
                        Tenant:   tenantID,
9✔
1769
                        IsDevice: true,
9✔
1770
                })
9✔
1771
        }
9✔
1772
        d.listDeviceDeployments(ctx, w, r, false)
9✔
1773
}
1774

1775
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1776
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
27✔
1777
        l := requestlog.GetRequestLogger(r)
27✔
1778

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

1793
        page, perPage, err := rest_utils.ParsePagination(r)
26✔
1794
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
29✔
1795
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
3✔
1796
        }
3✔
1797
        if err != nil {
32✔
1798
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
6✔
1799
                return
6✔
1800
        }
6✔
1801

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

1816
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
17✔
1817
        if err != nil {
20✔
1818
                d.view.RenderInternalError(w, r, err, l)
3✔
1819
                return
3✔
1820
        }
3✔
1821
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
14✔
1822

14✔
1823
        hasNext := totalCount > lq.Skip+len(deps)
14✔
1824
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
14✔
1825
        for _, l := range links {
28✔
1826
                w.Header().Add("Link", l)
14✔
1827
        }
14✔
1828

1829
        d.view.RenderSuccessGet(w, deps)
14✔
1830
}
1831

1832
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
1833
        r *rest.Request) {
×
1834
        ctx := r.Context()
×
1835
        tenantID := r.PathParam("tenantID")
×
1836
        if tenantID != "" {
×
1837
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1838
                        Tenant:   tenantID,
×
1839
                        IsDevice: true,
×
1840
                })
×
1841
        }
×
1842

1843
        l := requestlog.GetRequestLogger(r)
×
1844

×
1845
        id := r.PathParam("id")
×
1846

×
1847
        // Decommission deployments for devices and update deployment stats
×
1848
        err := d.app.DecommissionDevice(ctx, id)
×
1849

×
1850
        switch err {
×
1851
        case nil, app.ErrStorageNotFound:
×
1852
                d.view.RenderEmptySuccessResponse(w)
×
1853
        default:
×
1854
                d.view.RenderInternalError(w, r, err, l)
×
1855

1856
        }
1857
}
1858

1859
// tenants
1860

1861
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1862
        ctx := r.Context()
1✔
1863
        l := requestlog.GetRequestLogger(r)
1✔
1864

1✔
1865
        defer r.Body.Close()
1✔
1866

1✔
1867
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1868
        if err != nil {
2✔
1869
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1870
                return
1✔
1871
        }
1✔
1872

1873
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1874
        if err != nil {
1✔
1875
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1876
                return
×
1877
        }
×
1878

1879
        w.WriteHeader(http.StatusCreated)
1✔
1880
}
1881

1882
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1883
        w rest.ResponseWriter,
1884
        r *rest.Request,
1885
) {
6✔
1886
        tenantID := r.PathParam("tenant")
6✔
1887
        if tenantID == "" {
7✔
1888
                l := requestlog.GetRequestLogger(r)
1✔
1889
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1890
                return
1✔
1891
        }
1✔
1892

1893
        r.Request = r.WithContext(identity.WithContext(
5✔
1894
                r.Context(),
5✔
1895
                &identity.Identity{Tenant: tenantID},
5✔
1896
        ))
5✔
1897
        d.LookupDeployment(w, r)
5✔
1898
}
1899

1900
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1901
        w rest.ResponseWriter,
1902
        r *rest.Request,
1903
) {
5✔
1904
        l := requestlog.GetRequestLogger(r)
5✔
1905

5✔
1906
        tenantID := r.PathParam("tenant")
5✔
1907

5✔
1908
        ctx := identity.WithContext(
5✔
1909
                r.Context(),
5✔
1910
                &identity.Identity{Tenant: tenantID},
5✔
1911
        )
5✔
1912

5✔
1913
        settings, err := d.app.GetStorageSettings(ctx)
5✔
1914
        if err != nil {
7✔
1915
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1916
                return
2✔
1917
        }
2✔
1918

1919
        d.view.RenderSuccessGet(w, settings)
3✔
1920
}
1921

1922
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1923
        w rest.ResponseWriter,
1924
        r *rest.Request,
1925
) {
10✔
1926
        l := requestlog.GetRequestLogger(r)
10✔
1927

10✔
1928
        defer r.Body.Close()
10✔
1929

10✔
1930
        tenantID := r.PathParam("tenant")
10✔
1931

10✔
1932
        ctx := identity.WithContext(
10✔
1933
                r.Context(),
10✔
1934
                &identity.Identity{Tenant: tenantID},
10✔
1935
        )
10✔
1936

10✔
1937
        settings, err := model.ParseStorageSettingsRequest(r.Body)
10✔
1938
        if err != nil {
13✔
1939
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
1940
                return
3✔
1941
        }
3✔
1942

1943
        err = d.app.SetStorageSettings(ctx, settings)
8✔
1944
        if err != nil {
10✔
1945
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1946
                return
2✔
1947
        }
2✔
1948

1949
        w.WriteHeader(http.StatusNoContent)
6✔
1950
}
1951

1952
func (d *DeploymentsApiHandlers) PutReleaseTags(
1953
        w rest.ResponseWriter,
1954
        r *rest.Request,
1955
) {
8✔
1956
        ctx := r.Context()
8✔
1957
        l := log.FromContext(ctx)
8✔
1958

8✔
1959
        releaseName := r.PathParam(ParamName)
8✔
1960
        if releaseName == "" {
9✔
1961
                err := errors.New("path parameter 'release_name' cannot be empty")
1✔
1962
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusNotFound)
1✔
1963
                return
1✔
1964
        }
1✔
1965

1966
        var tags model.Tags
7✔
1967
        dec := json.NewDecoder(r.Body)
7✔
1968
        if err := dec.Decode(&tags); err != nil {
8✔
1969
                rest_utils.RestErrWithLog(w, r, l,
1✔
1970
                        errors.WithMessage(err,
1✔
1971
                                "malformed JSON in request body"),
1✔
1972
                        http.StatusBadRequest)
1✔
1973
                return
1✔
1974
        }
1✔
1975
        if err := tags.Validate(); err != nil {
7✔
1976
                rest_utils.RestErrWithLog(w, r, l,
1✔
1977
                        errors.WithMessage(err,
1✔
1978
                                "invalid request body"),
1✔
1979
                        http.StatusBadRequest)
1✔
1980
                return
1✔
1981
        }
1✔
1982

1983
        err := d.app.ReplaceReleaseTags(ctx, releaseName, tags)
5✔
1984
        if err != nil {
8✔
1985
                status := http.StatusInternalServerError
3✔
1986
                if errors.Is(err, app.ErrReleaseNotFound) {
4✔
1987
                        status = http.StatusNotFound
1✔
1988
                } else if errors.Is(err, model.ErrTooManyUniqueTags) {
4✔
1989
                        status = http.StatusConflict
1✔
1990
                }
1✔
1991
                rest_utils.RestErrWithLog(w, r, l, err, status)
3✔
1992
                return
3✔
1993
        }
1994

1995
        w.WriteHeader(http.StatusNoContent)
2✔
1996
}
1997

1998
func (d *DeploymentsApiHandlers) GetReleaseTagKeys(
1999
        w rest.ResponseWriter,
2000
        r *rest.Request,
2001
) {
3✔
2002
        ctx := r.Context()
3✔
2003
        l := log.FromContext(ctx)
3✔
2004

3✔
2005
        tags, err := d.app.ListReleaseTags(ctx)
3✔
2006
        if err != nil {
5✔
2007
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusInternalServerError)
2✔
2008
                return
2✔
2009
        }
2✔
2010

2011
        w.WriteHeader(http.StatusOK)
1✔
2012
        err = w.WriteJson(tags)
1✔
2013
        if err != nil {
1✔
NEW
2014
                l.Errorf("failed to serialize JSON response: %s", err.Error())
×
NEW
2015
        }
×
2016
}
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