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

mendersoftware / deployments / 1265080747

23 Apr 2024 11:36AM UTC coverage: 79.606% (-0.05%) from 79.655%
1265080747

push

gitlab-ci

web-flow
Merge pull request #1013 from alfrunes/MEN-7175

fix: Improve error message when uploading too large artifacts

2 of 7 new or added lines in 1 file covered. (28.57%)

2 existing lines in 1 file now uncovered.

8045 of 10106 relevant lines covered (79.61%)

34.45 hits per line

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

76.72
/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
        DefaultMaxGenerateDataSize = 512 * 1024 * 1024       // 512MiB
58

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

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

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

89
const Redacted = "REDACTED"
90

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

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

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

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

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

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

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

145
        EnableDirectUpload bool
146
        // EnableDirectUploadSkipVerify allows turning off the verification of uploaded artifacts
147
        EnableDirectUploadSkipVerify bool
148

149
        // DisableNewReleasesFeature is a flag that turns off the new API end-points
150
        // related to releases; helpful in performing long-running maintenance and data
151
        // migrations on the artifacts and releases collections.
152
        DisableNewReleasesFeature bool
153
}
154

155
func NewConfig() *Config {
211✔
156
        return &Config{
211✔
157
                PresignExpire:       DefaultDownloadLinkExpire,
211✔
158
                PresignScheme:       "https",
211✔
159
                MaxImageSize:        DefaultMaxImageSize,
211✔
160
                MaxGenerateDataSize: DefaultMaxGenerateDataSize,
211✔
161
        }
211✔
162
}
211✔
163

164
func (conf *Config) SetPresignSecret(key []byte) *Config {
20✔
165
        conf.PresignSecret = key
20✔
166
        return conf
20✔
167
}
20✔
168

169
func (conf *Config) SetPresignExpire(duration time.Duration) *Config {
14✔
170
        conf.PresignExpire = duration
14✔
171
        return conf
14✔
172
}
14✔
173

174
func (conf *Config) SetPresignHostname(hostname string) *Config {
12✔
175
        conf.PresignHostname = hostname
12✔
176
        return conf
12✔
177
}
12✔
178

179
func (conf *Config) SetPresignScheme(scheme string) *Config {
14✔
180
        conf.PresignScheme = scheme
14✔
181
        return conf
14✔
182
}
14✔
183

184
func (conf *Config) SetMaxImageSize(size int64) *Config {
1✔
185
        conf.MaxImageSize = size
1✔
186
        return conf
1✔
187
}
1✔
188

189
func (conf *Config) SetMaxGenerateDataSize(size int64) *Config {
1✔
190
        conf.MaxGenerateDataSize = size
1✔
191
        return conf
1✔
192
}
1✔
193

194
func (conf *Config) SetEnableDirectUpload(enable bool) *Config {
12✔
195
        conf.EnableDirectUpload = enable
12✔
196
        return conf
12✔
197
}
12✔
198

199
func (conf *Config) SetEnableDirectUploadSkipVerify(enable bool) *Config {
1✔
200
        conf.EnableDirectUploadSkipVerify = enable
1✔
201
        return conf
1✔
202
}
1✔
203

204
func (conf *Config) SetDisableNewReleasesFeature(disable bool) *Config {
3✔
205
        conf.DisableNewReleasesFeature = disable
3✔
206
        return conf
3✔
207
}
3✔
208

209
type DeploymentsApiHandlers struct {
210
        view   RESTView
211
        store  store.DataStore
212
        app    app.App
213
        config Config
214
}
215

216
func NewDeploymentsApiHandlers(
217
        store store.DataStore,
218
        view RESTView,
219
        app app.App,
220
        config ...*Config,
221
) *DeploymentsApiHandlers {
180✔
222
        conf := NewConfig()
180✔
223
        for _, c := range config {
214✔
224
                if c == nil {
35✔
225
                        continue
1✔
226
                }
227
                if c.PresignSecret != nil {
53✔
228
                        conf.PresignSecret = c.PresignSecret
20✔
229
                }
20✔
230
                if c.PresignExpire != 0 {
64✔
231
                        conf.PresignExpire = c.PresignExpire
31✔
232
                }
31✔
233
                if c.PresignHostname != "" {
45✔
234
                        conf.PresignHostname = c.PresignHostname
12✔
235
                }
12✔
236
                if c.PresignScheme != "" {
64✔
237
                        conf.PresignScheme = c.PresignScheme
31✔
238
                }
31✔
239
                if c.MaxImageSize > 0 {
64✔
240
                        conf.MaxImageSize = c.MaxImageSize
31✔
241
                }
31✔
242
                if c.MaxGenerateDataSize > 0 {
64✔
243
                        conf.MaxGenerateDataSize = c.MaxGenerateDataSize
31✔
244
                }
31✔
245
                conf.DisableNewReleasesFeature = c.DisableNewReleasesFeature
33✔
246
                conf.EnableDirectUpload = c.EnableDirectUpload
33✔
247
                conf.EnableDirectUploadSkipVerify = c.EnableDirectUploadSkipVerify
33✔
248
        }
249
        return &DeploymentsApiHandlers{
180✔
250
                store:  store,
180✔
251
                view:   view,
180✔
252
                app:    app,
180✔
253
                config: *conf,
180✔
254
        }
180✔
255
}
256

257
func (d *DeploymentsApiHandlers) AliveHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
258
        w.WriteHeader(http.StatusNoContent)
1✔
259
}
1✔
260

261
func (d *DeploymentsApiHandlers) HealthHandler(w rest.ResponseWriter, r *rest.Request) {
2✔
262
        ctx := r.Context()
2✔
263
        l := log.FromContext(ctx)
2✔
264
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
2✔
265
        defer cancel()
2✔
266

2✔
267
        err := d.app.HealthCheck(ctx)
2✔
268
        if err != nil {
3✔
269
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
270
                return
1✔
271
        }
1✔
272
        w.WriteHeader(http.StatusNoContent)
1✔
273
}
274

275
func getReleaseOrImageFilter(r *rest.Request, version listReleasesVersion,
276
        paginated bool) *model.ReleaseOrImageFilter {
32✔
277

32✔
278
        q := r.URL.Query()
32✔
279

32✔
280
        filter := &model.ReleaseOrImageFilter{
32✔
281
                Name:       q.Get(ParamName),
32✔
282
                UpdateType: q.Get(ParamUpdateType),
32✔
283
        }
32✔
284
        if version == listReleasesV1 {
57✔
285
                filter.Description = q.Get(ParamDescription)
25✔
286
                filter.DeviceType = q.Get(ParamDeviceType)
25✔
287
        } else if version == listReleasesV2 {
41✔
288
                filter.Tags = q[ParamTag]
8✔
289
                for i, t := range filter.Tags {
12✔
290
                        filter.Tags[i] = strings.ToLower(t)
4✔
291
                }
4✔
292
        }
293

294
        if paginated {
49✔
295
                filter.Sort = q.Get(ParamSort)
17✔
296
                if page := q.Get(ParamPage); page != "" {
18✔
297
                        if i, err := strconv.Atoi(page); err == nil {
2✔
298
                                filter.Page = i
1✔
299
                        }
1✔
300
                }
301
                if perPage := q.Get(ParamPerPage); perPage != "" {
19✔
302
                        if i, err := strconv.Atoi(perPage); err == nil {
4✔
303
                                filter.PerPage = i
2✔
304
                        }
2✔
305
                }
306
                if filter.Page <= 0 {
33✔
307
                        filter.Page = 1
16✔
308
                }
16✔
309
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
33✔
310
                        filter.PerPage = DefaultPerPage
16✔
311
                }
16✔
312
        }
313

314
        return filter
32✔
315
}
316

317
type limitResponse struct {
318
        Limit uint64 `json:"limit"`
319
        Usage uint64 `json:"usage"`
320
}
321

322
func (d *DeploymentsApiHandlers) GetLimit(w rest.ResponseWriter, r *rest.Request) {
3✔
323
        l := requestlog.GetRequestLogger(r)
3✔
324

3✔
325
        name := r.PathParam("name")
3✔
326

3✔
327
        if !model.IsValidLimit(name) {
4✔
328
                d.view.RenderError(w, r,
1✔
329
                        errors.Errorf("unsupported limit %s", name),
1✔
330
                        http.StatusBadRequest, l)
1✔
331
                return
1✔
332
        }
1✔
333

334
        limit, err := d.app.GetLimit(r.Context(), name)
2✔
335
        if err != nil {
3✔
336
                d.view.RenderInternalError(w, r, err, l)
1✔
337
                return
1✔
338
        }
1✔
339

340
        d.view.RenderSuccessGet(w, limitResponse{
1✔
341
                Limit: limit.Value,
1✔
342
                Usage: 0, // TODO fill this when ready
1✔
343
        })
1✔
344
}
345

346
// images
347

348
func (d *DeploymentsApiHandlers) GetImage(w rest.ResponseWriter, r *rest.Request) {
1✔
349
        l := requestlog.GetRequestLogger(r)
1✔
350

1✔
351
        id := r.PathParam("id")
1✔
352

1✔
353
        if !govalidator.IsUUID(id) {
2✔
354
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
355
                return
1✔
356
        }
1✔
357

358
        image, err := d.app.GetImage(r.Context(), id)
1✔
359
        if err != nil {
1✔
360
                d.view.RenderInternalError(w, r, err, l)
×
361
                return
×
362
        }
×
363

364
        if image == nil {
2✔
365
                d.view.RenderErrorNotFound(w, r, l)
1✔
366
                return
1✔
367
        }
1✔
368

369
        d.view.RenderSuccessGet(w, image)
1✔
370
}
371

372
func (d *DeploymentsApiHandlers) GetImages(w rest.ResponseWriter, r *rest.Request) {
5✔
373
        l := requestlog.GetRequestLogger(r)
5✔
374

5✔
375
        defer redactReleaseName(r)
5✔
376
        filter := getReleaseOrImageFilter(r, listReleasesV1, false)
5✔
377

5✔
378
        list, _, err := d.app.ListImages(r.Context(), filter)
5✔
379
        if err != nil {
6✔
380
                d.view.RenderInternalError(w, r, err, l)
1✔
381
                return
1✔
382
        }
1✔
383

384
        d.view.RenderSuccessGet(w, list)
4✔
385
}
386

387
func (d *DeploymentsApiHandlers) ListImages(w rest.ResponseWriter, r *rest.Request) {
4✔
388
        l := requestlog.GetRequestLogger(r)
4✔
389

4✔
390
        defer redactReleaseName(r)
4✔
391
        filter := getReleaseOrImageFilter(r, listReleasesV1, true)
4✔
392

4✔
393
        list, totalCount, err := d.app.ListImages(r.Context(), filter)
4✔
394
        if err != nil {
5✔
395
                d.view.RenderInternalError(w, r, err, l)
1✔
396
                return
1✔
397
        }
1✔
398

399
        hasNext := totalCount > int(filter.Page*filter.PerPage)
3✔
400
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
3✔
401
        for _, l := range links {
6✔
402
                w.Header().Add("Link", l)
3✔
403
        }
3✔
404
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
3✔
405

3✔
406
        d.view.RenderSuccessGet(w, list)
3✔
407
}
408

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

1✔
412
        id := r.PathParam("id")
1✔
413

1✔
414
        if !govalidator.IsUUID(id) {
1✔
415
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
416
                return
×
417
        }
×
418

419
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
420
        link, err := d.app.DownloadLink(r.Context(), id, time.Duration(expireSeconds)*time.Second)
1✔
421
        if err != nil {
1✔
422
                d.view.RenderInternalError(w, r, err, l)
×
423
                return
×
424
        }
×
425

426
        if link == nil {
1✔
427
                d.view.RenderErrorNotFound(w, r, l)
×
428
                return
×
429
        }
×
430

431
        d.view.RenderSuccessGet(w, link)
1✔
432
}
433

434
func (d *DeploymentsApiHandlers) UploadLink(w rest.ResponseWriter, r *rest.Request) {
4✔
435
        l := requestlog.GetRequestLogger(r)
4✔
436

4✔
437
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
4✔
438
        link, err := d.app.UploadLink(
4✔
439
                r.Context(),
4✔
440
                time.Duration(expireSeconds)*time.Second,
4✔
441
                d.config.EnableDirectUploadSkipVerify,
4✔
442
        )
4✔
443
        if err != nil {
5✔
444
                d.view.RenderInternalError(w, r, err, l)
1✔
445
                return
1✔
446
        }
1✔
447

448
        if link == nil {
4✔
449
                d.view.RenderErrorNotFound(w, r, l)
1✔
450
                return
1✔
451
        }
1✔
452

453
        d.view.RenderSuccessGet(w, link)
2✔
454
}
455

456
const maxMetadataSize = 2048
457

458
func (d *DeploymentsApiHandlers) CompleteUpload(w rest.ResponseWriter, r *rest.Request) {
4✔
459
        ctx := r.Context()
4✔
460
        l := log.FromContext(ctx)
4✔
461

4✔
462
        artifactID := r.PathParam(ParamID)
4✔
463

4✔
464
        var metadata *model.DirectUploadMetadata
4✔
465
        if d.config.EnableDirectUploadSkipVerify {
5✔
466
                var directMetadata model.DirectUploadMetadata
1✔
467
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
468
                n, err := io.ReadFull(r.Body, bodyBuffer)
1✔
469
                r.Body.Close()
1✔
470
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
471
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
472
                } else {
1✔
473
                        err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
474
                        if err == nil {
2✔
475
                                if directMetadata.Validate() == nil {
2✔
476
                                        metadata = &directMetadata
1✔
477
                                }
1✔
478
                        } else {
1✔
479
                                l.Errorf("error parsing json data: %s", err.Error())
1✔
480
                        }
1✔
481
                }
482
        }
483

484
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
4✔
485
        switch errors.Cause(err) {
4✔
486
        case nil:
2✔
487
                // w.Header().Set("Link", "FEAT: Upload status API")
2✔
488
                w.WriteHeader(http.StatusAccepted)
2✔
489
        case app.ErrUploadNotFound:
1✔
490
                d.view.RenderErrorNotFound(w, r, l)
1✔
491
        default:
1✔
492
                l.Error(err)
1✔
493
                w.WriteHeader(http.StatusInternalServerError)
1✔
494
                w.WriteJson(rest_utils.ApiError{ // nolint:errcheck
1✔
495
                        Err:   "internal server error",
1✔
496
                        ReqId: requestid.FromContext(ctx),
1✔
497
                })
1✔
498
        }
499
}
500

501
func (d *DeploymentsApiHandlers) DownloadConfiguration(w rest.ResponseWriter, r *rest.Request) {
10✔
502
        if d.config.PresignSecret == nil {
11✔
503
                rest.NotFound(w, r)
1✔
504
                return
1✔
505
        }
1✔
506
        var (
9✔
507
                deviceID, _     = url.PathUnescape(r.PathParam(ParamDeviceID))
9✔
508
                deviceType, _   = url.PathUnescape(r.PathParam(ParamDeviceType))
9✔
509
                deploymentID, _ = url.PathUnescape(r.PathParam(ParamDeploymentID))
9✔
510
        )
9✔
511
        if deviceID == "" || deviceType == "" || deploymentID == "" {
9✔
512
                rest.NotFound(w, r)
×
513
                return
×
514
        }
×
515

516
        var (
9✔
517
                tenantID string
9✔
518
                l        = log.FromContext(r.Context())
9✔
519
                q        = r.URL.Query()
9✔
520
                err      error
9✔
521
        )
9✔
522
        tenantID = q.Get(ParamTenantID)
9✔
523
        sig := model.NewRequestSignature(r.Request, d.config.PresignSecret)
9✔
524
        if err = sig.Validate(); err != nil {
12✔
525
                switch cause := errors.Cause(err); cause {
3✔
526
                case model.ErrLinkExpired:
1✔
527
                        d.view.RenderError(w, r, cause, http.StatusForbidden, l)
1✔
528
                default:
2✔
529
                        d.view.RenderError(w, r,
2✔
530
                                errors.Wrap(err, "invalid request parameters"),
2✔
531
                                http.StatusBadRequest, l,
2✔
532
                        )
2✔
533
                }
534
                return
3✔
535
        }
536

537
        if !sig.VerifyHMAC256() {
9✔
538
                d.view.RenderError(w, r,
2✔
539
                        errors.New("signature invalid"),
2✔
540
                        http.StatusForbidden, l,
2✔
541
                )
2✔
542
                return
2✔
543
        }
2✔
544

545
        // Validate request signature
546
        ctx := identity.WithContext(r.Context(), &identity.Identity{
6✔
547
                Subject:  deviceID,
6✔
548
                Tenant:   tenantID,
6✔
549
                IsDevice: true,
6✔
550
        })
6✔
551

6✔
552
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
6✔
553
        if err != nil {
8✔
554
                switch cause := errors.Cause(err); cause {
2✔
555
                case app.ErrModelDeploymentNotFound:
1✔
556
                        d.view.RenderError(w, r,
1✔
557
                                errors.Errorf(
1✔
558
                                        "deployment with id '%s' not found",
1✔
559
                                        deploymentID,
1✔
560
                                ),
1✔
561
                                http.StatusNotFound, l,
1✔
562
                        )
1✔
563
                default:
1✔
564
                        l.Error(err.Error())
1✔
565
                        d.view.RenderInternalError(w, r, err, l)
1✔
566
                }
567
                return
2✔
568
        }
569
        artifactPayload, err := io.ReadAll(artifact)
4✔
570
        if err != nil {
5✔
571
                l.Error(err.Error())
1✔
572
                d.view.RenderInternalError(w, r, err, l)
1✔
573
                return
1✔
574
        }
1✔
575

576
        rw := w.(http.ResponseWriter)
3✔
577
        hdr := rw.Header()
3✔
578
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
3✔
579
        hdr.Set("Content-Type", app.ArtifactContentType)
3✔
580
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
3✔
581
        rw.WriteHeader(http.StatusOK)
3✔
582
        _, err = rw.Write(artifactPayload)
3✔
583
        if err != nil {
3✔
584
                // There's not anything we can do here in terms of the response.
×
585
                l.Error(err.Error())
×
586
        }
×
587
}
588

589
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
1✔
590
        l := requestlog.GetRequestLogger(r)
1✔
591

1✔
592
        id := r.PathParam("id")
1✔
593

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

599
        if err := d.app.DeleteImage(r.Context(), id); err != nil {
2✔
600
                switch err {
1✔
601
                default:
×
602
                        d.view.RenderInternalError(w, r, err, l)
×
603
                case app.ErrImageMetaNotFound:
×
604
                        d.view.RenderErrorNotFound(w, r, l)
×
605
                case app.ErrModelImageInActiveDeployment:
1✔
606
                        d.view.RenderError(w, r, ErrArtifactUsedInActiveDeployment, http.StatusConflict, l)
1✔
607
                }
608
                return
1✔
609
        }
610

611
        d.view.RenderSuccessDelete(w)
1✔
612
}
613

614
func (d *DeploymentsApiHandlers) EditImage(w rest.ResponseWriter, r *rest.Request) {
×
615
        l := requestlog.GetRequestLogger(r)
×
616

×
617
        id := r.PathParam("id")
×
618

×
619
        if !govalidator.IsUUID(id) {
×
620
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
621
                return
×
622
        }
×
623

624
        constructor, err := getImageMetaFromBody(r)
×
625
        if err != nil {
×
626
                d.view.RenderError(
×
627
                        w,
×
628
                        r,
×
629
                        errors.Wrap(err, "Validating request body"),
×
630
                        http.StatusBadRequest,
×
631
                        l,
×
632
                )
×
633
                return
×
634
        }
×
635

636
        found, err := d.app.EditImage(r.Context(), id, constructor)
×
637
        if err != nil {
×
638
                if err == app.ErrModelImageUsedInAnyDeployment {
×
639
                        d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
640
                        return
×
641
                }
×
642
                d.view.RenderInternalError(w, r, err, l)
×
643
                return
×
644
        }
645

646
        if !found {
×
647
                d.view.RenderErrorNotFound(w, r, l)
×
648
                return
×
649
        }
×
650

651
        d.view.RenderSuccessPut(w)
×
652
}
653

654
func getImageMetaFromBody(r *rest.Request) (*model.ImageMeta, error) {
×
655

×
656
        var constructor *model.ImageMeta
×
657

×
658
        if err := r.DecodeJsonPayload(&constructor); err != nil {
×
659
                return nil, err
×
660
        }
×
661

662
        if err := constructor.Validate(); err != nil {
×
663
                return nil, err
×
664
        }
×
665

666
        return constructor, nil
×
667
}
668

669
// NewImage is the Multipart Image/Meta upload handler.
670
// Request should be of type "multipart/form-data". The parts are
671
// key/value pairs of metadata information except the last one,
672
// which must contain the artifact file.
673
func (d *DeploymentsApiHandlers) NewImage(w rest.ResponseWriter, r *rest.Request) {
7✔
674
        d.newImageWithContext(r.Context(), w, r)
7✔
675
}
7✔
676

677
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(w rest.ResponseWriter, r *rest.Request) {
7✔
678
        l := requestlog.GetRequestLogger(r)
7✔
679

7✔
680
        tenantID := r.PathParam("tenant")
7✔
681

7✔
682
        if tenantID == "" {
7✔
683
                rest_utils.RestErrWithLog(
×
684
                        w,
×
685
                        r,
×
686
                        l,
×
687
                        fmt.Errorf("missing tenant id in path"),
×
688
                        http.StatusBadRequest,
×
689
                )
×
690
                return
×
691
        }
×
692

693
        var ctx context.Context
7✔
694
        if tenantID != "default" {
8✔
695
                ident := &identity.Identity{Tenant: tenantID}
1✔
696
                ctx = identity.WithContext(r.Context(), ident)
1✔
697
        } else {
7✔
698
                ctx = r.Context()
6✔
699
        }
6✔
700

701
        d.newImageWithContext(ctx, w, r)
7✔
702
}
703

704
func (d *DeploymentsApiHandlers) newImageWithContext(
705
        ctx context.Context,
706
        w rest.ResponseWriter,
707
        r *rest.Request,
708
) {
13✔
709
        l := requestlog.GetRequestLogger(r)
13✔
710

13✔
711
        formReader, err := r.MultipartReader()
13✔
712
        if err != nil {
17✔
713
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
714
                return
4✔
715
        }
4✔
716

717
        // parse multipart message
718
        multipartUploadMsg, err := d.ParseMultipart(formReader)
9✔
719

9✔
720
        if err != nil {
14✔
721
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
5✔
722
                return
5✔
723
        }
5✔
724

725
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
5✔
726
        if err == nil {
8✔
727
                d.view.RenderSuccessPost(w, r, imgID)
3✔
728
                return
3✔
729
        }
3✔
730
        var cErr *model.ConflictError
3✔
731
        if errors.As(err, &cErr) {
5✔
732
                w.WriteHeader(http.StatusConflict)
2✔
733
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
2✔
734
                err = w.WriteJson(cErr)
2✔
735
                if err != nil {
2✔
736
                        l.Error(err)
×
737
                } else {
2✔
738
                        l.Error(cErr.Error())
2✔
739
                }
2✔
740
                return
2✔
741
        }
742
        cause := errors.Cause(err)
1✔
743
        switch cause {
1✔
744
        default:
×
745
                d.view.RenderInternalError(w, r, err, l)
×
746
                return
×
747
        case app.ErrModelArtifactNotUnique:
×
748
                l.Error(err.Error())
×
749
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
750
                return
×
751
        case app.ErrModelParsingArtifactFailed:
1✔
752
                l.Error(err.Error())
1✔
753
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
754
                return
1✔
NEW
755
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
NEW
756
                d.view.RenderError(w, r, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge, l)
×
NEW
757
                return
×
758
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
759
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
NEW
760
                io.ErrUnexpectedEOF:
×
761
                l.Error(err.Error())
×
762
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
763
                return
×
764
        }
765
}
766

767
func formatArtifactUploadError(err error) error {
2✔
768
        // remove generic message
2✔
769
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
770

2✔
771
        // handle specific cases
2✔
772

2✔
773
        if strings.Contains(errMsg, "invalid checksum") {
2✔
774
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
775
        }
×
776

777
        if strings.Contains(errMsg, "unsupported version") {
2✔
778
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
779
                        "; supported versions are: 1, 2")
×
780
        }
×
781

782
        return errors.New(errMsg)
2✔
783
}
784

785
// GenerateImage s the multipart Raw Data/Meta upload handler.
786
// Request should be of type "multipart/form-data". The parts are
787
// key/valyue pairs of metadata information except the last one,
788
// which must contain the file containing the raw data to be processed
789
// into an artifact.
790
func (d *DeploymentsApiHandlers) GenerateImage(w rest.ResponseWriter, r *rest.Request) {
14✔
791
        l := requestlog.GetRequestLogger(r)
14✔
792

14✔
793
        formReader, err := r.MultipartReader()
14✔
794
        if err != nil {
17✔
795
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
3✔
796
                return
3✔
797
        }
3✔
798

799
        // parse multipart message
800
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
11✔
801
        if err != nil {
15✔
802
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
4✔
803
                return
4✔
804
        }
4✔
805

806
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
7✔
807
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
14✔
808
                multipartMsg.Token = tokenFields[1]
7✔
809
        }
7✔
810

811
        imgID, err := d.app.GenerateImage(r.Context(), multipartMsg)
7✔
812
        cause := errors.Cause(err)
7✔
813
        switch cause {
7✔
814
        default:
1✔
815
                d.view.RenderInternalError(w, r, err, l)
1✔
816
        case nil:
3✔
817
                d.view.RenderSuccessPost(w, r, imgID)
3✔
818
        case app.ErrModelArtifactNotUnique:
1✔
819
                l.Error(err.Error())
1✔
820
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
1✔
821
        case app.ErrModelParsingArtifactFailed:
1✔
822
                l.Error(err.Error())
1✔
823
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
824
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
825
                d.view.RenderError(w, r, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge, l)
1✔
826
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
827
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
NEW
828
                io.ErrUnexpectedEOF:
×
UNCOV
829
                l.Error(err.Error())
×
UNCOV
830
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
831
        }
832
}
833

834
// ParseMultipart parses multipart/form-data message.
835
func (d *DeploymentsApiHandlers) ParseMultipart(
836
        r *multipart.Reader,
837
) (*model.MultipartUploadMsg, error) {
9✔
838
        uploadMsg := &model.MultipartUploadMsg{
9✔
839
                MetaConstructor: &model.ImageMeta{},
9✔
840
        }
9✔
841
        var size int64
9✔
842
        // Parse the multipart form sequentially. To remain backward compatible
9✔
843
        // all form names that are not part of the API are ignored.
9✔
844
        for {
38✔
845
                part, err := r.NextPart()
29✔
846
                if err != nil {
31✔
847
                        if err == io.EOF {
4✔
848
                                // The whole message has been consumed without
2✔
849
                                // the "artifact" form part.
2✔
850
                                return nil, ErrArtifactFileMissing
2✔
851
                        }
2✔
852
                        return nil, err
×
853
                }
854
                switch strings.ToLower(part.FormName()) {
27✔
855
                case "description":
7✔
856
                        // Add description to the metadata
7✔
857
                        dscr, err := io.ReadAll(part)
7✔
858
                        if err != nil {
7✔
859
                                return nil, err
×
860
                        }
×
861
                        uploadMsg.MetaConstructor.Description = string(dscr)
7✔
862

863
                case "size":
7✔
864
                        // Add size limit to the metadata
7✔
865
                        sz, err := io.ReadAll(part)
7✔
866
                        if err != nil {
7✔
867
                                return nil, err
×
868
                        }
×
869
                        size, err = strconv.ParseInt(string(sz), 10, 64)
7✔
870
                        if err != nil {
7✔
871
                                return nil, err
×
872
                        }
×
873
                        if size > d.config.MaxImageSize {
7✔
874
                                return nil, ErrModelArtifactFileTooLarge
×
875
                        }
×
876

877
                case "artifact_id":
7✔
878
                        // Add artifact id to the metadata (must be a valid UUID).
7✔
879
                        b, err := io.ReadAll(part)
7✔
880
                        if err != nil {
7✔
881
                                return nil, err
×
882
                        }
×
883
                        id := string(b)
7✔
884
                        if !govalidator.IsUUID(id) {
10✔
885
                                return nil, errors.New(
3✔
886
                                        "artifact_id is not a valid UUID",
3✔
887
                                )
3✔
888
                        }
3✔
889
                        uploadMsg.ArtifactID = id
4✔
890

891
                case "artifact":
5✔
892
                        // Assign the form-data payload to the artifact reader
5✔
893
                        // and return. The content is consumed elsewhere.
5✔
894
                        if size > 0 {
10✔
895
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
5✔
896
                        } else {
5✔
897
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
×
898
                                        part,
×
899
                                        d.config.MaxImageSize,
×
900
                                )
×
901
                        }
×
902
                        return uploadMsg, nil
5✔
903

904
                default:
4✔
905
                        // Ignore all non-API sections.
4✔
906
                        continue
4✔
907
                }
908
        }
909
}
910

911
// ParseGenerateImageMultipart parses multipart/form-data message.
912
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
913
        r *multipart.Reader,
914
) (*model.MultipartGenerateImageMsg, error) {
11✔
915
        msg := &model.MultipartGenerateImageMsg{}
11✔
916
        var size int64
11✔
917

11✔
918
ParseLoop:
11✔
919
        for {
65✔
920
                part, err := r.NextPart()
54✔
921
                if err != nil {
56✔
922
                        if err == io.EOF {
4✔
923
                                break
2✔
924
                        }
925
                        return nil, err
×
926
                }
927
                switch strings.ToLower(part.FormName()) {
52✔
928
                case "args":
7✔
929
                        b, err := io.ReadAll(part)
7✔
930
                        if err != nil {
7✔
931
                                return nil, errors.Wrap(err,
×
932
                                        "failed to read form value 'args'",
×
933
                                )
×
934
                        }
×
935
                        msg.Args = string(b)
7✔
936

937
                case "description":
7✔
938
                        b, err := io.ReadAll(part)
7✔
939
                        if err != nil {
7✔
940
                                return nil, errors.Wrap(err,
×
941
                                        "failed to read form value 'description'",
×
942
                                )
×
943
                        }
×
944
                        msg.Description = string(b)
7✔
945

946
                case "device_types_compatible":
9✔
947
                        b, err := io.ReadAll(part)
9✔
948
                        if err != nil {
9✔
949
                                return nil, errors.Wrap(err,
×
950
                                        "failed to read form value 'device_types_compatible'",
×
951
                                )
×
952
                        }
×
953
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
9✔
954

955
                case "file":
9✔
956
                        if size > 0 {
15✔
957
                                msg.FileReader = utils.ReadExactly(part, size)
6✔
958
                        } else {
9✔
959
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxGenerateDataSize)
3✔
960
                        }
3✔
961
                        break ParseLoop
9✔
962

963
                case "name":
10✔
964
                        b, err := io.ReadAll(part)
10✔
965
                        if err != nil {
10✔
966
                                return nil, errors.Wrap(err,
×
967
                                        "failed to read form value 'name'",
×
968
                                )
×
969
                        }
×
970
                        msg.Name = string(b)
10✔
971

972
                case "type":
9✔
973
                        b, err := io.ReadAll(part)
9✔
974
                        if err != nil {
9✔
975
                                return nil, errors.Wrap(err,
×
976
                                        "failed to read form value 'type'",
×
977
                                )
×
978
                        }
×
979
                        msg.Type = string(b)
9✔
980

981
                case "size":
6✔
982
                        // Add size limit to the metadata
6✔
983
                        sz, err := io.ReadAll(part)
6✔
984
                        if err != nil {
6✔
985
                                return nil, err
×
986
                        }
×
987
                        size, err = strconv.ParseInt(string(sz), 10, 64)
6✔
988
                        if err != nil {
6✔
989
                                return nil, err
×
990
                        }
×
991
                        if size > d.config.MaxGenerateDataSize {
6✔
992
                                return nil, ErrModelArtifactFileTooLarge
×
993
                        }
×
994

995
                default:
×
996
                        // Ignore non-API sections.
×
997
                        continue
×
998
                }
999
        }
1000

1001
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
11✔
1002
}
1003

1004
// deployments
1005
func (d *DeploymentsApiHandlers) createDeployment(
1006
        w rest.ResponseWriter,
1007
        r *rest.Request,
1008
        ctx context.Context,
1009
        l *log.Logger,
1010
        group string,
1011
) {
13✔
1012
        constructor, err := d.getDeploymentConstructorFromBody(r, group)
13✔
1013
        if err != nil {
19✔
1014
                d.view.RenderError(
6✔
1015
                        w,
6✔
1016
                        r,
6✔
1017
                        errors.Wrap(err, "Validating request body"),
6✔
1018
                        http.StatusBadRequest,
6✔
1019
                        l,
6✔
1020
                )
6✔
1021
                return
6✔
1022
        }
6✔
1023

1024
        id, err := d.app.CreateDeployment(ctx, constructor)
8✔
1025
        switch err {
8✔
1026
        case nil:
4✔
1027
                // in case of deployment to group remove "/group/{name}" from path before creating location
4✔
1028
                // haeder
4✔
1029
                r.URL.Path = strings.TrimSuffix(r.URL.Path, "/group/"+constructor.Group)
4✔
1030
                d.view.RenderSuccessPost(w, r, id)
4✔
1031
        case app.ErrNoArtifact:
1✔
1032
                d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
1✔
1033
        case app.ErrNoDevices:
2✔
1034
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
2✔
1035
        default:
2✔
1036
                d.view.RenderInternalError(w, r, err, l)
2✔
1037
        }
1038
}
1039

1040
func (d *DeploymentsApiHandlers) PostDeployment(w rest.ResponseWriter, r *rest.Request) {
8✔
1041
        ctx := r.Context()
8✔
1042
        l := requestlog.GetRequestLogger(r)
8✔
1043

8✔
1044
        d.createDeployment(w, r, ctx, l, "")
8✔
1045
}
8✔
1046

1047
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
5✔
1048
        ctx := r.Context()
5✔
1049
        l := requestlog.GetRequestLogger(r)
5✔
1050

5✔
1051
        group := r.PathParam("name")
5✔
1052
        if len(group) < 1 {
5✔
1053
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
1054
        }
×
1055
        d.createDeployment(w, r, ctx, l, group)
5✔
1056
}
1057

1058
// parseDeviceConfigurationDeploymentPathParams parses expected params
1059
// and check if the params are not empty
1060
func parseDeviceConfigurationDeploymentPathParams(r *rest.Request) (string, string, string, error) {
7✔
1061
        tenantID := r.PathParam("tenant")
7✔
1062
        deviceID := r.PathParam(ParamDeviceID)
7✔
1063
        if deviceID == "" {
7✔
1064
                return "", "", "", errors.New("device ID missing")
×
1065
        }
×
1066
        deploymentID := r.PathParam(ParamDeploymentID)
7✔
1067
        if deploymentID == "" {
7✔
1068
                return "", "", "", errors.New("deployment ID missing")
×
1069
        }
×
1070
        return tenantID, deviceID, deploymentID, nil
7✔
1071
}
1072

1073
// getConfigurationDeploymentConstructorFromBody extracts configuration
1074
// deployment constructor from the request body and validates it
1075
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
1076
        *model.ConfigurationDeploymentConstructor, error) {
7✔
1077

7✔
1078
        var constructor *model.ConfigurationDeploymentConstructor
7✔
1079

7✔
1080
        if err := r.DecodeJsonPayload(&constructor); err != nil {
8✔
1081
                return nil, err
1✔
1082
        }
1✔
1083

1084
        if err := constructor.Validate(); err != nil {
8✔
1085
                return nil, err
2✔
1086
        }
2✔
1087

1088
        return constructor, nil
5✔
1089
}
1090

1091
// device configuration deployment handler
1092
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1093
        w rest.ResponseWriter,
1094
        r *rest.Request,
1095
) {
7✔
1096
        l := requestlog.GetRequestLogger(r)
7✔
1097

7✔
1098
        // get path params
7✔
1099
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
7✔
1100
        if err != nil {
7✔
1101
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
1102
                return
×
1103
        }
×
1104

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

7✔
1108
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
7✔
1109
        if err != nil {
10✔
1110
                d.view.RenderError(
3✔
1111
                        w,
3✔
1112
                        r,
3✔
1113
                        errors.Wrap(err, "Validating request body"),
3✔
1114
                        http.StatusBadRequest,
3✔
1115
                        l,
3✔
1116
                )
3✔
1117
                return
3✔
1118
        }
3✔
1119

1120
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
5✔
1121
        switch err {
5✔
1122
        default:
1✔
1123
                d.view.RenderInternalError(w, r, err, l)
1✔
1124
        case nil:
3✔
1125
                r.URL.Path = "./deployments"
3✔
1126
                d.view.RenderSuccessPost(w, r, id)
3✔
1127
        case app.ErrDuplicateDeployment:
2✔
1128
                d.view.RenderError(w, r, err, http.StatusConflict, l)
2✔
1129
        case app.ErrInvalidDeploymentID:
1✔
1130
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1131
        }
1132
}
1133

1134
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1135
        r *rest.Request,
1136
        group string,
1137
) (*model.DeploymentConstructor, error) {
13✔
1138
        var constructor *model.DeploymentConstructor
13✔
1139
        if err := r.DecodeJsonPayload(&constructor); err != nil {
16✔
1140
                return nil, err
3✔
1141
        }
3✔
1142

1143
        constructor.Group = group
11✔
1144

11✔
1145
        if err := constructor.ValidateNew(); err != nil {
15✔
1146
                return nil, err
4✔
1147
        }
4✔
1148

1149
        return constructor, nil
8✔
1150
}
1151

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

1✔
1156
        id := r.PathParam("id")
1✔
1157

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

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

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

1174
        d.view.RenderSuccessGet(w, deployment)
1✔
1175
}
1176

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

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

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

1188
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1189
        if err != nil {
1✔
1190
                d.view.RenderInternalError(w, r, err, l)
×
1191
                return
×
1192
        }
×
1193

1194
        if stats == nil {
1✔
1195
                d.view.RenderErrorNotFound(w, r, l)
×
1196
                return
×
1197
        }
×
1198

1199
        d.view.RenderSuccessGet(w, stats)
1✔
1200
}
1201

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

4✔
1204
        ctx := r.Context()
4✔
1205
        l := requestlog.GetRequestLogger(r)
4✔
1206

4✔
1207
        ids := model.DeploymentIDs{}
4✔
1208
        if err := r.DecodeJsonPayload(&ids); err != nil {
4✔
1209
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1210
                return
×
1211
        }
×
1212

1213
        if len(ids.IDs) == 0 {
4✔
1214
                w.WriteHeader(http.StatusOK)
×
1215
                _ = w.WriteJson(struct{}{})
×
1216
                return
×
1217
        }
×
1218

1219
        if err := ids.Validate(); err != nil {
5✔
1220
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1221
                return
1✔
1222
        }
1✔
1223

1224
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
3✔
1225
        if err != nil {
5✔
1226
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
3✔
1227
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
1✔
1228
                        return
1✔
1229
                }
1✔
1230
                d.view.RenderInternalError(w, r, err, l)
1✔
1231
                return
1✔
1232
        }
1233

1234
        w.WriteHeader(http.StatusOK)
1✔
1235

1✔
1236
        _ = w.WriteJson(stats)
1✔
1237
}
1238

1239
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
1240
        ctx := r.Context()
×
1241
        l := requestlog.GetRequestLogger(r)
×
1242

×
1243
        id := r.PathParam("id")
×
1244

×
1245
        if !govalidator.IsUUID(id) {
×
1246
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1247
                return
×
1248
        }
×
1249

1250
        deployment, err := d.app.GetDeployment(ctx, id)
×
1251
        if err != nil {
×
1252
                d.view.RenderInternalError(w, r, err, l)
×
1253
                return
×
1254
        }
×
1255

1256
        if deployment == nil {
×
1257
                d.view.RenderErrorNotFound(w, r, l)
×
1258
                return
×
1259
        }
×
1260

1261
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1262
}
1263

1264
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1265
        ctx := r.Context()
1✔
1266
        l := requestlog.GetRequestLogger(r)
1✔
1267

1✔
1268
        id := r.PathParam("id")
1✔
1269

1✔
1270
        if !govalidator.IsUUID(id) {
1✔
1271
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1272
                return
×
1273
        }
×
1274

1275
        // receive request body
1276
        var status struct {
1✔
1277
                Status model.DeviceDeploymentStatus
1✔
1278
        }
1✔
1279

1✔
1280
        err := r.DecodeJsonPayload(&status)
1✔
1281
        if err != nil {
1✔
1282
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1283
                return
×
1284
        }
×
1285
        // "aborted" is the only supported status
1286
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1287
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
1288
        }
×
1289

1290
        l.Infof("Abort deployment: %s", id)
1✔
1291

1✔
1292
        // Check if deployment is finished
1✔
1293
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1294
        if err != nil {
1✔
1295
                d.view.RenderInternalError(w, r, err, l)
×
1296
                return
×
1297
        }
×
1298
        if isDeploymentFinished {
2✔
1299
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
1✔
1300
                return
1✔
1301
        }
1✔
1302

1303
        // Abort deployments for devices and update deployment stats
1304
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1305
                d.view.RenderInternalError(w, r, err, l)
×
1306
        }
×
1307

1308
        d.view.RenderEmptySuccessResponse(w)
1✔
1309
}
1310

1311
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
12✔
1312
        var (
12✔
1313
                installed *model.InstalledDeviceDeployment
12✔
1314
                ctx       = r.Context()
12✔
1315
                l         = requestlog.GetRequestLogger(r)
12✔
1316
                idata     = identity.FromContext(ctx)
12✔
1317
        )
12✔
1318
        if idata == nil {
14✔
1319
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
2✔
1320
                return
2✔
1321
        }
2✔
1322

1323
        q := r.URL.Query()
11✔
1324
        defer func() {
22✔
1325
                var reEncode bool = false
11✔
1326
                if name := q.Get(ParamArtifactName); name != "" {
20✔
1327
                        q.Set(ParamArtifactName, Redacted)
9✔
1328
                        reEncode = true
9✔
1329
                }
9✔
1330
                if typ := q.Get(ParamDeviceType); typ != "" {
20✔
1331
                        q.Set(ParamDeviceType, Redacted)
9✔
1332
                        reEncode = true
9✔
1333
                }
9✔
1334
                if reEncode {
20✔
1335
                        r.URL.RawQuery = q.Encode()
9✔
1336
                }
9✔
1337
        }()
1338
        if strings.EqualFold(r.Method, http.MethodPost) {
13✔
1339
                // POST
2✔
1340
                installed = new(model.InstalledDeviceDeployment)
2✔
1341
                if err := r.DecodeJsonPayload(&installed); err != nil {
3✔
1342
                        d.view.RenderError(w, r,
1✔
1343
                                errors.Wrap(err, "invalid schema"),
1✔
1344
                                http.StatusBadRequest, l)
1✔
1345
                        return
1✔
1346
                }
1✔
1347
        } else {
9✔
1348
                // GET or HEAD
9✔
1349
                installed = &model.InstalledDeviceDeployment{
9✔
1350
                        ArtifactName: q.Get(ParamArtifactName),
9✔
1351
                        DeviceType:   q.Get(ParamDeviceType),
9✔
1352
                }
9✔
1353
        }
9✔
1354

1355
        if err := installed.Validate(); err != nil {
11✔
1356
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1357
                return
1✔
1358
        }
1✔
1359

1360
        request := &model.DeploymentNextRequest{
9✔
1361
                DeviceProvides: installed,
9✔
1362
        }
9✔
1363

9✔
1364
        d.getDeploymentForDevice(w, r, idata, request)
9✔
1365
}
1366

1367
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1368
        w rest.ResponseWriter,
1369
        r *rest.Request,
1370
        idata *identity.Identity,
1371
        request *model.DeploymentNextRequest,
1372
) {
9✔
1373
        ctx := r.Context()
9✔
1374
        l := requestlog.GetRequestLogger(r)
9✔
1375

9✔
1376
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
9✔
1377
        if err != nil {
11✔
1378
                if err == app.ErrConflictingRequestData {
3✔
1379
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1380
                } else {
2✔
1381
                        d.view.RenderInternalError(w, r, err, l)
1✔
1382
                }
1✔
1383
                return
2✔
1384
        }
1385

1386
        if deployment == nil {
10✔
1387
                d.view.RenderNoUpdateForDevice(w)
2✔
1388
                return
2✔
1389
        } else if deployment.Type == model.DeploymentTypeConfiguration {
14✔
1390
                // Generate pre-signed URL
5✔
1391
                var hostName string = d.config.PresignHostname
5✔
1392
                if hostName == "" {
7✔
1393
                        if hostName = r.Header.Get(hdrForwardedHost); hostName == "" {
3✔
1394
                                d.view.RenderInternalError(w, r,
1✔
1395
                                        errors.New("presign.hostname not configured; "+
1✔
1396
                                                "unable to generate download link "+
1✔
1397
                                                " for configuration deployment"), l)
1✔
1398
                                return
1✔
1399
                        }
1✔
1400
                }
1401
                req, _ := http.NewRequest(
4✔
1402
                        http.MethodGet,
4✔
1403
                        FMTConfigURL(
4✔
1404
                                d.config.PresignScheme, hostName,
4✔
1405
                                deployment.ID, request.DeviceProvides.DeviceType,
4✔
1406
                                idata.Subject,
4✔
1407
                        ),
4✔
1408
                        nil,
4✔
1409
                )
4✔
1410
                if idata.Tenant != "" {
7✔
1411
                        q := req.URL.Query()
3✔
1412
                        q.Set(model.ParamTenantID, idata.Tenant)
3✔
1413
                        req.URL.RawQuery = q.Encode()
3✔
1414
                }
3✔
1415
                sig := model.NewRequestSignature(req, d.config.PresignSecret)
4✔
1416
                expireTS := time.Now().Add(d.config.PresignExpire)
4✔
1417
                sig.SetExpire(expireTS)
4✔
1418
                deployment.Artifact.Source = model.Link{
4✔
1419
                        Uri:    sig.PresignURL(),
4✔
1420
                        Expire: expireTS,
4✔
1421
                }
4✔
1422
        }
1423

1424
        d.view.RenderSuccessGet(w, deployment)
6✔
1425
}
1426

1427
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1428
        w rest.ResponseWriter,
1429
        r *rest.Request,
1430
) {
1✔
1431
        ctx := r.Context()
1✔
1432
        l := requestlog.GetRequestLogger(r)
1✔
1433

1✔
1434
        did := r.PathParam("id")
1✔
1435

1✔
1436
        idata := identity.FromContext(ctx)
1✔
1437
        if idata == nil {
1✔
1438
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1439
                return
×
1440
        }
×
1441

1442
        // receive request body
1443
        var report model.StatusReport
1✔
1444

1✔
1445
        err := r.DecodeJsonPayload(&report)
1✔
1446
        if err != nil {
1✔
1447
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1448
                return
×
1449
        }
×
1450

1451
        l.Infof("status: %+v", report)
1✔
1452
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
1✔
1453
                idata.Subject, model.DeviceDeploymentState{
1✔
1454
                        Status:   report.Status,
1✔
1455
                        SubState: report.SubState,
1✔
1456
                }); err != nil {
1✔
1457

×
1458
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
1459
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
1460
                } else if err == app.ErrStorageNotFound {
×
1461
                        d.view.RenderErrorNotFound(w, r, l)
×
1462
                } else {
×
1463
                        d.view.RenderInternalError(w, r, err, l)
×
1464
                }
×
1465
                return
×
1466
        }
1467

1468
        d.view.RenderEmptySuccessResponse(w)
1✔
1469
}
1470

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

1✔
1478
        did := r.PathParam("id")
1✔
1479

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

1485
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
1✔
1486
        if err != nil {
1✔
1487
                switch err {
×
1488
                case app.ErrModelDeploymentNotFound:
×
1489
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1490
                        return
×
1491
                default:
×
1492
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1493
                        return
×
1494
                }
1495
        }
1496

1497
        d.view.RenderSuccessGet(w, statuses)
1✔
1498
}
1499

1500
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1501
        w rest.ResponseWriter,
1502
        r *rest.Request,
1503
) {
1✔
1504
        ctx := r.Context()
1✔
1505
        l := requestlog.GetRequestLogger(r)
1✔
1506

1✔
1507
        did := r.PathParam("id")
1✔
1508

1✔
1509
        if !govalidator.IsUUID(did) {
1✔
1510
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1511
                return
×
1512
        }
×
1513

1514
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1515
        if err != nil {
1✔
1516
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1517
                return
×
1518
        }
×
1519

1520
        lq := store.ListQuery{
1✔
1521
                Skip:         int((page - 1) * perPage),
1✔
1522
                Limit:        int(perPage),
1✔
1523
                DeploymentID: did,
1✔
1524
        }
1✔
1525
        if status := r.URL.Query().Get("status"); status != "" {
1✔
1526
                lq.Status = &status
×
1527
        }
×
1528
        if err = lq.Validate(); err != nil {
1✔
1529
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1530
                return
×
1531
        }
×
1532

1533
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1534
        if err != nil {
1✔
1535
                switch err {
×
1536
                case app.ErrModelDeploymentNotFound:
×
1537
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1538
                        return
×
1539
                default:
×
1540
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1541
                        return
×
1542
                }
1543
        }
1544

1545
        hasNext := totalCount > int(page*perPage)
1✔
1546
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1547
        for _, l := range links {
2✔
1548
                w.Header().Add("Link", l)
1✔
1549
        }
1✔
1550
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1551
        d.view.RenderSuccessGet(w, statuses)
1✔
1552
}
1553

1554
func ParseLookupQuery(vals url.Values) (model.Query, error) {
9✔
1555
        query := model.Query{}
9✔
1556

9✔
1557
        search := vals.Get("search")
9✔
1558
        if search != "" {
9✔
1559
                query.SearchText = search
×
1560
        }
×
1561

1562
        createdBefore := vals.Get("created_before")
9✔
1563
        if createdBefore != "" {
10✔
1564
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
2✔
1565
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1566
                } else {
1✔
1567
                        query.CreatedBefore = &createdBeforeTime
×
1568
                }
×
1569
        }
1570

1571
        createdAfter := vals.Get("created_after")
8✔
1572
        if createdAfter != "" {
8✔
1573
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1574
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1575
                } else {
×
1576
                        query.CreatedAfter = &createdAfterTime
×
1577
                }
×
1578
        }
1579

1580
        switch strings.ToLower(vals.Get("sort")) {
8✔
1581
        case model.SortDirectionAscending:
1✔
1582
                query.Sort = model.SortDirectionAscending
1✔
1583
        case "", model.SortDirectionDescending:
7✔
1584
                query.Sort = model.SortDirectionDescending
7✔
1585
        default:
×
1586
                return query, ErrInvalidSortDirection
×
1587
        }
1588

1589
        status := vals.Get("status")
8✔
1590
        switch status {
8✔
1591
        case "inprogress":
×
1592
                query.Status = model.StatusQueryInProgress
×
1593
        case "finished":
×
1594
                query.Status = model.StatusQueryFinished
×
1595
        case "pending":
×
1596
                query.Status = model.StatusQueryPending
×
1597
        case "aborted":
×
1598
                query.Status = model.StatusQueryAborted
×
1599
        case "":
8✔
1600
                query.Status = model.StatusQueryAny
8✔
1601
        default:
×
1602
                return query, errors.Errorf("unknown status %s", status)
×
1603

1604
        }
1605

1606
        dType := vals.Get("type")
8✔
1607
        if dType == "" {
16✔
1608
                return query, nil
8✔
1609
        }
8✔
1610
        deploymentType := model.DeploymentType(dType)
×
1611
        if deploymentType == model.DeploymentTypeSoftware ||
×
1612
                deploymentType == model.DeploymentTypeConfiguration {
×
1613
                query.Type = deploymentType
×
1614
        } else {
×
1615
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1616
        }
×
1617

1618
        return query, nil
×
1619
}
1620

1621
func parseEpochToTimestamp(epoch string) (time.Time, error) {
1✔
1622
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
2✔
1623
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
1✔
1624
        } else {
1✔
1625
                return time.Unix(epochInt64, 0).UTC(), nil
×
1626
        }
×
1627
}
1628

1629
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
9✔
1630
        ctx := r.Context()
9✔
1631
        l := requestlog.GetRequestLogger(r)
9✔
1632
        q := r.URL.Query()
9✔
1633
        defer func() {
18✔
1634
                if search := q.Get("search"); search != "" {
9✔
1635
                        q.Set("search", Redacted)
×
1636
                        r.URL.RawQuery = q.Encode()
×
1637
                }
×
1638
        }()
1639

1640
        query, err := ParseLookupQuery(q)
9✔
1641
        if err != nil {
10✔
1642
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1643
                return
1✔
1644
        }
1✔
1645

1646
        page, perPage, err := rest_utils.ParsePagination(r)
8✔
1647
        if err != nil {
9✔
1648
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1649
                return
1✔
1650
        }
1✔
1651
        query.Skip = int((page - 1) * perPage)
7✔
1652
        query.Limit = int(perPage + 1)
7✔
1653

7✔
1654
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
7✔
1655
        if err != nil {
8✔
1656
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1657
                return
1✔
1658
        }
1✔
1659
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
6✔
1660

6✔
1661
        len := len(deps)
6✔
1662
        hasNext := false
6✔
1663
        if uint64(len) > perPage {
6✔
1664
                hasNext = true
×
1665
                len = int(perPage)
×
1666
        }
×
1667

1668
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
6✔
1669
        for _, l := range links {
13✔
1670
                w.Header().Add("Link", l)
7✔
1671
        }
7✔
1672

1673
        d.view.RenderSuccessGet(w, deps[:len])
6✔
1674
}
1675

1676
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1677
        ctx := r.Context()
1✔
1678
        l := requestlog.GetRequestLogger(r)
1✔
1679

1✔
1680
        did := r.PathParam("id")
1✔
1681

1✔
1682
        idata := identity.FromContext(ctx)
1✔
1683
        if idata == nil {
1✔
1684
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1685
                return
×
1686
        }
×
1687

1688
        // reuse DeploymentLog, device and deployment IDs are ignored when
1689
        // (un-)marshaling DeploymentLog to/from JSON
1690
        var log model.DeploymentLog
1✔
1691

1✔
1692
        err := r.DecodeJsonPayload(&log)
1✔
1693
        if err != nil {
1✔
1694
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1695
                return
×
1696
        }
×
1697

1698
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1699
                did, log.Messages); err != nil {
1✔
1700

×
1701
                if err == app.ErrModelDeploymentNotFound {
×
1702
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1703
                } else {
×
1704
                        d.view.RenderInternalError(w, r, err, l)
×
1705
                }
×
1706
                return
×
1707
        }
1708

1709
        d.view.RenderEmptySuccessResponse(w)
1✔
1710
}
1711

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

1✔
1716
        did := r.PathParam("id")
1✔
1717
        devid := r.PathParam("devid")
1✔
1718

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

1✔
1721
        if err != nil {
1✔
1722
                d.view.RenderInternalError(w, r, err, l)
×
1723
                return
×
1724
        }
×
1725

1726
        if depl == nil {
1✔
1727
                d.view.RenderErrorNotFound(w, r, l)
×
1728
                return
×
1729
        }
×
1730

1731
        d.view.RenderDeploymentLog(w, *depl)
1✔
1732
}
1733

1734
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
3✔
1735
        ctx := r.Context()
3✔
1736
        l := requestlog.GetRequestLogger(r)
3✔
1737

3✔
1738
        id := r.PathParam("id")
3✔
1739
        err := d.app.AbortDeviceDeployments(ctx, id)
3✔
1740

3✔
1741
        switch err {
3✔
1742
        case nil, app.ErrStorageNotFound:
2✔
1743
                d.view.RenderEmptySuccessResponse(w)
2✔
1744
        default:
1✔
1745
                d.view.RenderInternalError(w, r, err, l)
1✔
1746
        }
1747
}
1748

1749
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1750
        r *rest.Request) {
3✔
1751
        ctx := r.Context()
3✔
1752
        l := requestlog.GetRequestLogger(r)
3✔
1753

3✔
1754
        id := r.PathParam("id")
3✔
1755
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
3✔
1756

3✔
1757
        switch err {
3✔
1758
        case nil, app.ErrStorageNotFound:
2✔
1759
                d.view.RenderEmptySuccessResponse(w)
2✔
1760
        default:
1✔
1761
                d.view.RenderInternalError(w, r, err, l)
1✔
1762
        }
1763
}
1764

1765
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
9✔
1766
        ctx := r.Context()
9✔
1767
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1768
}
9✔
1769

1770
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1771
        r *rest.Request) {
9✔
1772
        ctx := r.Context()
9✔
1773
        tenantID := r.PathParam("tenant")
9✔
1774
        if tenantID != "" {
18✔
1775
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1776
                        Tenant:   tenantID,
9✔
1777
                        IsDevice: true,
9✔
1778
                })
9✔
1779
        }
9✔
1780
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1781
}
1782

1783
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(w rest.ResponseWriter,
1784
        r *rest.Request) {
9✔
1785
        ctx := r.Context()
9✔
1786
        tenantID := r.PathParam("tenant")
9✔
1787
        if tenantID != "" {
18✔
1788
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1789
                        Tenant:   tenantID,
9✔
1790
                        IsDevice: true,
9✔
1791
                })
9✔
1792
        }
9✔
1793
        d.listDeviceDeployments(ctx, w, r, false)
9✔
1794
}
1795

1796
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1797
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
27✔
1798
        l := requestlog.GetRequestLogger(r)
27✔
1799

27✔
1800
        did := ""
27✔
1801
        var IDs []string
27✔
1802
        if byDeviceID {
45✔
1803
                did = r.PathParam("id")
18✔
1804
        } else {
27✔
1805
                values := r.URL.Query()
9✔
1806
                if values.Has("id") && len(values["id"]) > 0 {
17✔
1807
                        IDs = values["id"]
8✔
1808
                } else {
9✔
1809
                        d.view.RenderError(w, r, ErrEmptyID, http.StatusBadRequest, l)
1✔
1810
                        return
1✔
1811
                }
1✔
1812
        }
1813

1814
        page, perPage, err := rest_utils.ParsePagination(r)
26✔
1815
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
29✔
1816
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
3✔
1817
        }
3✔
1818
        if err != nil {
32✔
1819
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
6✔
1820
                return
6✔
1821
        }
6✔
1822

1823
        lq := store.ListQueryDeviceDeployments{
20✔
1824
                Skip:     int((page - 1) * perPage),
20✔
1825
                Limit:    int(perPage),
20✔
1826
                DeviceID: did,
20✔
1827
                IDs:      IDs,
20✔
1828
        }
20✔
1829
        if status := r.URL.Query().Get("status"); status != "" {
26✔
1830
                lq.Status = &status
6✔
1831
        }
6✔
1832
        if err = lq.Validate(); err != nil {
23✔
1833
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
3✔
1834
                return
3✔
1835
        }
3✔
1836

1837
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
17✔
1838
        if err != nil {
20✔
1839
                d.view.RenderInternalError(w, r, err, l)
3✔
1840
                return
3✔
1841
        }
3✔
1842
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
14✔
1843

14✔
1844
        hasNext := totalCount > lq.Skip+len(deps)
14✔
1845
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
14✔
1846
        for _, l := range links {
28✔
1847
                w.Header().Add("Link", l)
14✔
1848
        }
14✔
1849

1850
        d.view.RenderSuccessGet(w, deps)
14✔
1851
}
1852

1853
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
1854
        r *rest.Request) {
×
1855
        ctx := r.Context()
×
1856
        tenantID := r.PathParam("tenantID")
×
1857
        if tenantID != "" {
×
1858
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1859
                        Tenant:   tenantID,
×
1860
                        IsDevice: true,
×
1861
                })
×
1862
        }
×
1863

1864
        l := requestlog.GetRequestLogger(r)
×
1865

×
1866
        id := r.PathParam("id")
×
1867

×
1868
        // Decommission deployments for devices and update deployment stats
×
1869
        err := d.app.DecommissionDevice(ctx, id)
×
1870

×
1871
        switch err {
×
1872
        case nil, app.ErrStorageNotFound:
×
1873
                d.view.RenderEmptySuccessResponse(w)
×
1874
        default:
×
1875
                d.view.RenderInternalError(w, r, err, l)
×
1876

1877
        }
1878
}
1879

1880
// tenants
1881

1882
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1883
        ctx := r.Context()
1✔
1884
        l := requestlog.GetRequestLogger(r)
1✔
1885

1✔
1886
        defer r.Body.Close()
1✔
1887

1✔
1888
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1889
        if err != nil {
2✔
1890
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1891
                return
1✔
1892
        }
1✔
1893

1894
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1895
        if err != nil {
1✔
1896
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1897
                return
×
1898
        }
×
1899

1900
        w.WriteHeader(http.StatusCreated)
1✔
1901
}
1902

1903
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1904
        w rest.ResponseWriter,
1905
        r *rest.Request,
1906
) {
6✔
1907
        tenantID := r.PathParam("tenant")
6✔
1908
        if tenantID == "" {
7✔
1909
                l := requestlog.GetRequestLogger(r)
1✔
1910
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1911
                return
1✔
1912
        }
1✔
1913

1914
        r.Request = r.WithContext(identity.WithContext(
5✔
1915
                r.Context(),
5✔
1916
                &identity.Identity{Tenant: tenantID},
5✔
1917
        ))
5✔
1918
        d.LookupDeployment(w, r)
5✔
1919
}
1920

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

5✔
1927
        tenantID := r.PathParam("tenant")
5✔
1928

5✔
1929
        ctx := identity.WithContext(
5✔
1930
                r.Context(),
5✔
1931
                &identity.Identity{Tenant: tenantID},
5✔
1932
        )
5✔
1933

5✔
1934
        settings, err := d.app.GetStorageSettings(ctx)
5✔
1935
        if err != nil {
7✔
1936
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1937
                return
2✔
1938
        }
2✔
1939

1940
        d.view.RenderSuccessGet(w, settings)
3✔
1941
}
1942

1943
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1944
        w rest.ResponseWriter,
1945
        r *rest.Request,
1946
) {
10✔
1947
        l := requestlog.GetRequestLogger(r)
10✔
1948

10✔
1949
        defer r.Body.Close()
10✔
1950

10✔
1951
        tenantID := r.PathParam("tenant")
10✔
1952

10✔
1953
        ctx := identity.WithContext(
10✔
1954
                r.Context(),
10✔
1955
                &identity.Identity{Tenant: tenantID},
10✔
1956
        )
10✔
1957

10✔
1958
        settings, err := model.ParseStorageSettingsRequest(r.Body)
10✔
1959
        if err != nil {
13✔
1960
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
1961
                return
3✔
1962
        }
3✔
1963

1964
        err = d.app.SetStorageSettings(ctx, settings)
8✔
1965
        if err != nil {
10✔
1966
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1967
                return
2✔
1968
        }
2✔
1969

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