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

mendersoftware / deployments / 1197570064

01 Mar 2024 06:24PM UTC coverage: 52.222% (-28.4%) from 80.645%
1197570064

Pull #998

gitlab-ci

web-flow
chore: bump github.com/Azure/azure-sdk-for-go/sdk/azcore

Bumps [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) from 1.9.1 to 1.10.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.9.1...sdk/azcore/v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azcore
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #998: chore: bump github.com/Azure/azure-sdk-for-go/sdk/azcore from 1.9.1 to 1.10.0

5218 of 9992 relevant lines covered (52.22%)

0.55 hits per line

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

52.88
/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() {
1✔
48
        rest.ErrorFieldName = "error"
1✔
49
}
1✔
50

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

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

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

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

88
const Redacted = "REDACTED"
89

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

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

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

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

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

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

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

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

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

153
func NewConfig() *Config {
1✔
154
        return &Config{
1✔
155
                PresignExpire: DefaultDownloadLinkExpire,
1✔
156
                PresignScheme: "https",
1✔
157
                MaxImageSize:  DefaultMaxImageSize,
1✔
158
        }
1✔
159
}
1✔
160

161
func (conf *Config) SetPresignSecret(key []byte) *Config {
1✔
162
        conf.PresignSecret = key
1✔
163
        return conf
1✔
164
}
1✔
165

166
func (conf *Config) SetPresignExpire(duration time.Duration) *Config {
1✔
167
        conf.PresignExpire = duration
1✔
168
        return conf
1✔
169
}
1✔
170

171
func (conf *Config) SetPresignHostname(hostname string) *Config {
1✔
172
        conf.PresignHostname = hostname
1✔
173
        return conf
1✔
174
}
1✔
175

176
func (conf *Config) SetPresignScheme(scheme string) *Config {
1✔
177
        conf.PresignScheme = scheme
1✔
178
        return conf
1✔
179
}
1✔
180

181
func (conf *Config) SetMaxImageSize(size int64) *Config {
1✔
182
        conf.MaxImageSize = size
1✔
183
        return conf
1✔
184
}
1✔
185

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

191
func (conf *Config) SetEnableDirectUploadSkipVerify(enable bool) *Config {
1✔
192
        conf.EnableDirectUploadSkipVerify = enable
1✔
193
        return conf
1✔
194
}
1✔
195

196
func (conf *Config) SetDisableNewReleasesFeature(disable bool) *Config {
1✔
197
        conf.DisableNewReleasesFeature = disable
1✔
198
        return conf
1✔
199
}
1✔
200

201
type DeploymentsApiHandlers struct {
202
        view   RESTView
203
        store  store.DataStore
204
        app    app.App
205
        config Config
206
}
207

208
func NewDeploymentsApiHandlers(
209
        store store.DataStore,
210
        view RESTView,
211
        app app.App,
212
        config ...*Config,
213
) *DeploymentsApiHandlers {
1✔
214
        conf := NewConfig()
1✔
215
        for _, c := range config {
2✔
216
                if c == nil {
1✔
217
                        continue
×
218
                }
219
                if c.PresignSecret != nil {
2✔
220
                        conf.PresignSecret = c.PresignSecret
1✔
221
                }
1✔
222
                if c.PresignExpire != 0 {
2✔
223
                        conf.PresignExpire = c.PresignExpire
1✔
224
                }
1✔
225
                if c.PresignHostname != "" {
2✔
226
                        conf.PresignHostname = c.PresignHostname
1✔
227
                }
1✔
228
                if c.PresignScheme != "" {
2✔
229
                        conf.PresignScheme = c.PresignScheme
1✔
230
                }
1✔
231
                if c.MaxImageSize > 0 {
2✔
232
                        conf.MaxImageSize = c.MaxImageSize
1✔
233
                }
1✔
234
                conf.DisableNewReleasesFeature = c.DisableNewReleasesFeature
1✔
235
                conf.EnableDirectUpload = c.EnableDirectUpload
1✔
236
                conf.EnableDirectUploadSkipVerify = c.EnableDirectUploadSkipVerify
1✔
237
        }
238
        return &DeploymentsApiHandlers{
1✔
239
                store:  store,
1✔
240
                view:   view,
1✔
241
                app:    app,
1✔
242
                config: *conf,
1✔
243
        }
1✔
244
}
245

246
func (d *DeploymentsApiHandlers) AliveHandler(w rest.ResponseWriter, r *rest.Request) {
×
247
        w.WriteHeader(http.StatusNoContent)
×
248
}
×
249

250
func (d *DeploymentsApiHandlers) HealthHandler(w rest.ResponseWriter, r *rest.Request) {
×
251
        ctx := r.Context()
×
252
        l := log.FromContext(ctx)
×
253
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
×
254
        defer cancel()
×
255

×
256
        err := d.app.HealthCheck(ctx)
×
257
        if err != nil {
×
258
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
×
259
                return
×
260
        }
×
261
        w.WriteHeader(http.StatusNoContent)
×
262
}
263

264
func getReleaseOrImageFilter(r *rest.Request, version listReleasesVersion,
265
        paginated bool) *model.ReleaseOrImageFilter {
1✔
266

1✔
267
        q := r.URL.Query()
1✔
268

1✔
269
        filter := &model.ReleaseOrImageFilter{
1✔
270
                Name:       q.Get(ParamName),
1✔
271
                UpdateType: q.Get(ParamUpdateType),
1✔
272
        }
1✔
273
        if version == listReleasesV1 {
2✔
274
                filter.Description = q.Get(ParamDescription)
1✔
275
                filter.DeviceType = q.Get(ParamDeviceType)
1✔
276
        } else if version == listReleasesV2 {
3✔
277
                filter.Tags = q[ParamTag]
1✔
278
                for i, t := range filter.Tags {
1✔
279
                        filter.Tags[i] = strings.ToLower(t)
×
280
                }
×
281
        }
282

283
        if paginated {
2✔
284
                filter.Sort = q.Get(ParamSort)
1✔
285
                if page := q.Get(ParamPage); page != "" {
1✔
286
                        if i, err := strconv.Atoi(page); err == nil {
×
287
                                filter.Page = i
×
288
                        }
×
289
                }
290
                if perPage := q.Get(ParamPerPage); perPage != "" {
1✔
291
                        if i, err := strconv.Atoi(perPage); err == nil {
×
292
                                filter.PerPage = i
×
293
                        }
×
294
                }
295
                if filter.Page <= 0 {
2✔
296
                        filter.Page = 1
1✔
297
                }
1✔
298
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
2✔
299
                        filter.PerPage = DefaultPerPage
1✔
300
                }
1✔
301
        }
302

303
        return filter
1✔
304
}
305

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

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

×
314
        name := r.PathParam("name")
×
315

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

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

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

335
// images
336

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

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

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

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

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

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

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

1✔
364
        defer redactReleaseName(r)
1✔
365
        filter := getReleaseOrImageFilter(r, listReleasesV1, false)
1✔
366

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

373
        d.view.RenderSuccessGet(w, list)
1✔
374
}
375

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

×
379
        defer redactReleaseName(r)
×
380
        filter := getReleaseOrImageFilter(r, listReleasesV1, true)
×
381

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

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

×
395
        d.view.RenderSuccessGet(w, list)
×
396
}
397

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

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

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

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

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

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

423
func (d *DeploymentsApiHandlers) UploadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
424
        l := requestlog.GetRequestLogger(r)
1✔
425

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

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

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

445
const maxMetadataSize = 2048
446

447
func (d *DeploymentsApiHandlers) CompleteUpload(w rest.ResponseWriter, r *rest.Request) {
1✔
448
        ctx := r.Context()
1✔
449
        l := log.FromContext(ctx)
1✔
450

1✔
451
        artifactID := r.PathParam(ParamID)
1✔
452

1✔
453
        var metadata *model.DirectUploadMetadata
1✔
454
        if d.config.EnableDirectUploadSkipVerify {
2✔
455
                var directMetadata model.DirectUploadMetadata
1✔
456
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
457
                n, err := io.ReadFull(r.Body, bodyBuffer)
1✔
458
                r.Body.Close()
1✔
459
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
460
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
461
                } else {
1✔
462
                        err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
463
                        if err == nil {
2✔
464
                                if directMetadata.Validate() == nil {
2✔
465
                                        metadata = &directMetadata
1✔
466
                                }
1✔
467
                        } else {
1✔
468
                                l.Errorf("error parsing json data: %s", err.Error())
1✔
469
                        }
1✔
470
                }
471
        }
472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
645
        var constructor *model.ImageMeta
×
646

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

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

655
        return constructor, nil
×
656
}
657

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

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

1✔
669
        tenantID := r.PathParam("tenant")
1✔
670

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

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

690
        d.newImageWithContext(ctx, w, r)
1✔
691
}
692

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

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

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

1✔
709
        if err != nil {
2✔
710
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
711
                return
1✔
712
        }
1✔
713

714
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
1✔
715
        if err == nil {
2✔
716
                d.view.RenderSuccessPost(w, r, imgID)
1✔
717
                return
1✔
718
        }
1✔
719
        var cErr *model.ConflictError
1✔
720
        if errors.As(err, &cErr) {
1✔
721
                w.WriteHeader(http.StatusConflict)
×
722
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
×
723
                err = w.WriteJson(cErr)
×
724
                if err != nil {
×
725
                        l.Error(err)
×
726
                } else {
×
727
                        l.Error(cErr.Error())
×
728
                }
×
729
                return
×
730
        }
731
        cause := errors.Cause(err)
1✔
732
        switch cause {
1✔
733
        default:
×
734
                d.view.RenderInternalError(w, r, err, l)
×
735
                return
×
736
        case app.ErrModelArtifactNotUnique:
×
737
                l.Error(err.Error())
×
738
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
739
                return
×
740
        case app.ErrModelParsingArtifactFailed:
1✔
741
                l.Error(err.Error())
1✔
742
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
743
                return
1✔
744
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
745
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
746
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
747
                l.Error(err.Error())
×
748
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
749
                return
×
750
        }
751
}
752

753
func formatArtifactUploadError(err error) error {
1✔
754
        // remove generic message
1✔
755
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
1✔
756

1✔
757
        // handle specific cases
1✔
758

1✔
759
        if strings.Contains(errMsg, "invalid checksum") {
1✔
760
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
761
        }
×
762

763
        if strings.Contains(errMsg, "unsupported version") {
1✔
764
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
765
                        "; supported versions are: 1, 2")
×
766
        }
×
767

768
        return errors.New(errMsg)
1✔
769
}
770

771
// GenerateImage s the multipart Raw Data/Meta upload handler.
772
// Request should be of type "multipart/form-data". The parts are
773
// key/valyue pairs of metadata information except the last one,
774
// which must contain the file containing the raw data to be processed
775
// into an artifact.
776
func (d *DeploymentsApiHandlers) GenerateImage(w rest.ResponseWriter, r *rest.Request) {
1✔
777
        l := requestlog.GetRequestLogger(r)
1✔
778

1✔
779
        formReader, err := r.MultipartReader()
1✔
780
        if err != nil {
1✔
781
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
782
                return
×
783
        }
×
784

785
        // parse multipart message
786
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
1✔
787
        if err != nil {
1✔
788
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
789
                return
×
790
        }
×
791

792
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
1✔
793
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
2✔
794
                multipartMsg.Token = tokenFields[1]
1✔
795
        }
1✔
796

797
        imgID, err := d.app.GenerateImage(r.Context(), multipartMsg)
1✔
798
        cause := errors.Cause(err)
1✔
799
        switch cause {
1✔
800
        default:
×
801
                d.view.RenderInternalError(w, r, err, l)
×
802
        case nil:
1✔
803
                d.view.RenderSuccessPost(w, r, imgID)
1✔
804
        case app.ErrModelArtifactNotUnique:
×
805
                l.Error(err.Error())
×
806
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
807
        case app.ErrModelParsingArtifactFailed:
×
808
                l.Error(err.Error())
×
809
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
×
810
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
811
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
812
                io.ErrUnexpectedEOF, utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
813
                l.Error(err.Error())
×
814
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
815
        }
816
}
817

818
// ParseMultipart parses multipart/form-data message.
819
func (d *DeploymentsApiHandlers) ParseMultipart(
820
        r *multipart.Reader,
821
) (*model.MultipartUploadMsg, error) {
1✔
822
        uploadMsg := &model.MultipartUploadMsg{
1✔
823
                MetaConstructor: &model.ImageMeta{},
1✔
824
        }
1✔
825
        var size int64
1✔
826
        // Parse the multipart form sequentially. To remain backward compatible
1✔
827
        // all form names that are not part of the API are ignored.
1✔
828
        for {
2✔
829
                part, err := r.NextPart()
1✔
830
                if err != nil {
1✔
831
                        if err == io.EOF {
×
832
                                // The whole message has been consumed without
×
833
                                // the "artifact" form part.
×
834
                                return nil, ErrArtifactFileMissing
×
835
                        }
×
836
                        return nil, err
×
837
                }
838
                switch strings.ToLower(part.FormName()) {
1✔
839
                case "description":
1✔
840
                        // Add description to the metadata
1✔
841
                        dscr, err := io.ReadAll(part)
1✔
842
                        if err != nil {
1✔
843
                                return nil, err
×
844
                        }
×
845
                        uploadMsg.MetaConstructor.Description = string(dscr)
1✔
846

847
                case "size":
1✔
848
                        // Add size limit to the metadata
1✔
849
                        sz, err := io.ReadAll(part)
1✔
850
                        if err != nil {
1✔
851
                                return nil, err
×
852
                        }
×
853
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
854
                        if err != nil {
1✔
855
                                return nil, err
×
856
                        }
×
857
                        // Add one since this will impose the upper limit on the
858
                        // artifact size.
859
                        if size > d.config.MaxImageSize {
1✔
860
                                return nil, ErrModelArtifactFileTooLarge
×
861
                        }
×
862

863
                case "artifact_id":
1✔
864
                        // Add artifact id to the metadata (must be a valid UUID).
1✔
865
                        b, err := io.ReadAll(part)
1✔
866
                        if err != nil {
1✔
867
                                return nil, err
×
868
                        }
×
869
                        id := string(b)
1✔
870
                        if !govalidator.IsUUID(id) {
2✔
871
                                return nil, errors.New(
1✔
872
                                        "artifact_id is not a valid UUID",
1✔
873
                                )
1✔
874
                        }
1✔
875
                        uploadMsg.ArtifactID = id
×
876

877
                case "artifact":
1✔
878
                        // Assign the form-data payload to the artifact reader
1✔
879
                        // and return. The content is consumed elsewhere.
1✔
880
                        if size > 0 {
2✔
881
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
1✔
882
                        } else {
1✔
883
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
×
884
                                        part,
×
885
                                        d.config.MaxImageSize,
×
886
                                )
×
887
                        }
×
888
                        return uploadMsg, nil
1✔
889

890
                default:
×
891
                        // Ignore all non-API sections.
×
892
                        continue
×
893
                }
894
        }
895
}
896

897
// ParseGenerateImageMultipart parses multipart/form-data message.
898
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
899
        r *multipart.Reader,
900
) (*model.MultipartGenerateImageMsg, error) {
1✔
901
        msg := &model.MultipartGenerateImageMsg{}
1✔
902
        var size int64
1✔
903

1✔
904
ParseLoop:
1✔
905
        for {
2✔
906
                part, err := r.NextPart()
1✔
907
                if err != nil {
1✔
908
                        if err == io.EOF {
×
909
                                break
×
910
                        }
911
                        return nil, err
×
912
                }
913
                switch strings.ToLower(part.FormName()) {
1✔
914
                case "args":
1✔
915
                        b, err := io.ReadAll(part)
1✔
916
                        if err != nil {
1✔
917
                                return nil, errors.Wrap(err,
×
918
                                        "failed to read form value 'args'",
×
919
                                )
×
920
                        }
×
921
                        msg.Args = string(b)
1✔
922

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

932
                case "device_types_compatible":
1✔
933
                        b, err := io.ReadAll(part)
1✔
934
                        if err != nil {
1✔
935
                                return nil, errors.Wrap(err,
×
936
                                        "failed to read form value 'device_types_compatible'",
×
937
                                )
×
938
                        }
×
939
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
1✔
940

941
                case "file":
1✔
942
                        if size > 0 {
1✔
943
                                msg.FileReader = utils.ReadExactly(part, size)
×
944
                        } else {
1✔
945
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxImageSize)
1✔
946
                        }
1✔
947
                        break ParseLoop
1✔
948

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

958
                case "type":
1✔
959
                        b, err := io.ReadAll(part)
1✔
960
                        if err != nil {
1✔
961
                                return nil, errors.Wrap(err,
×
962
                                        "failed to read form value 'type'",
×
963
                                )
×
964
                        }
×
965
                        msg.Type = string(b)
1✔
966

967
                case "size":
×
968
                        // Add size limit to the metadata
×
969
                        sz, err := io.ReadAll(part)
×
970
                        if err != nil {
×
971
                                return nil, err
×
972
                        }
×
973
                        size, err = strconv.ParseInt(string(sz), 10, 64)
×
974
                        if err != nil {
×
975
                                return nil, err
×
976
                        }
×
977
                        // Add one since this will impose the upper limit on the
978
                        // artifact size.
979
                        if size > d.config.MaxImageSize {
×
980
                                return nil, ErrModelArtifactFileTooLarge
×
981
                        }
×
982

983
                default:
×
984
                        // Ignore non-API sections.
×
985
                        continue
×
986
                }
987
        }
988

989
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
1✔
990
}
991

992
// deployments
993
func (d *DeploymentsApiHandlers) createDeployment(
994
        w rest.ResponseWriter,
995
        r *rest.Request,
996
        ctx context.Context,
997
        l *log.Logger,
998
        group string,
999
) {
1✔
1000
        constructor, err := d.getDeploymentConstructorFromBody(r, group)
1✔
1001
        if err != nil {
2✔
1002
                d.view.RenderError(
1✔
1003
                        w,
1✔
1004
                        r,
1✔
1005
                        errors.Wrap(err, "Validating request body"),
1✔
1006
                        http.StatusBadRequest,
1✔
1007
                        l,
1✔
1008
                )
1✔
1009
                return
1✔
1010
        }
1✔
1011

1012
        id, err := d.app.CreateDeployment(ctx, constructor)
1✔
1013
        switch err {
1✔
1014
        case nil:
1✔
1015
                // in case of deployment to group remove "/group/{name}" from path before creating location
1✔
1016
                // haeder
1✔
1017
                r.URL.Path = strings.TrimSuffix(r.URL.Path, "/group/"+constructor.Group)
1✔
1018
                d.view.RenderSuccessPost(w, r, id)
1✔
1019
        case app.ErrNoArtifact:
1✔
1020
                d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
1✔
1021
        case app.ErrNoDevices:
×
1022
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1023
        default:
×
1024
                d.view.RenderInternalError(w, r, err, l)
×
1025
        }
1026
}
1027

1028
func (d *DeploymentsApiHandlers) PostDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1029
        ctx := r.Context()
1✔
1030
        l := requestlog.GetRequestLogger(r)
1✔
1031

1✔
1032
        d.createDeployment(w, r, ctx, l, "")
1✔
1033
}
1✔
1034

1035
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
×
1036
        ctx := r.Context()
×
1037
        l := requestlog.GetRequestLogger(r)
×
1038

×
1039
        group := r.PathParam("name")
×
1040
        if len(group) < 1 {
×
1041
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
1042
        }
×
1043
        d.createDeployment(w, r, ctx, l, group)
×
1044
}
1045

1046
// parseDeviceConfigurationDeploymentPathParams parses expected params
1047
// and check if the params are not empty
1048
func parseDeviceConfigurationDeploymentPathParams(r *rest.Request) (string, string, string, error) {
1✔
1049
        tenantID := r.PathParam("tenant")
1✔
1050
        deviceID := r.PathParam(ParamDeviceID)
1✔
1051
        if deviceID == "" {
1✔
1052
                return "", "", "", errors.New("device ID missing")
×
1053
        }
×
1054
        deploymentID := r.PathParam(ParamDeploymentID)
1✔
1055
        if deploymentID == "" {
1✔
1056
                return "", "", "", errors.New("deployment ID missing")
×
1057
        }
×
1058
        return tenantID, deviceID, deploymentID, nil
1✔
1059
}
1060

1061
// getConfigurationDeploymentConstructorFromBody extracts configuration
1062
// deployment constructor from the request body and validates it
1063
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
1064
        *model.ConfigurationDeploymentConstructor, error) {
1✔
1065

1✔
1066
        var constructor *model.ConfigurationDeploymentConstructor
1✔
1067

1✔
1068
        if err := r.DecodeJsonPayload(&constructor); err != nil {
1✔
1069
                return nil, err
×
1070
        }
×
1071

1072
        if err := constructor.Validate(); err != nil {
2✔
1073
                return nil, err
1✔
1074
        }
1✔
1075

1076
        return constructor, nil
1✔
1077
}
1078

1079
// device configuration deployment handler
1080
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1081
        w rest.ResponseWriter,
1082
        r *rest.Request,
1083
) {
1✔
1084
        l := requestlog.GetRequestLogger(r)
1✔
1085

1✔
1086
        // get path params
1✔
1087
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
1✔
1088
        if err != nil {
1✔
1089
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
1090
                return
×
1091
        }
×
1092

1093
        // add tenant id to the context
1094
        ctx := identity.WithContext(r.Context(), &identity.Identity{Tenant: tenantID})
1✔
1095

1✔
1096
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
1✔
1097
        if err != nil {
2✔
1098
                d.view.RenderError(
1✔
1099
                        w,
1✔
1100
                        r,
1✔
1101
                        errors.Wrap(err, "Validating request body"),
1✔
1102
                        http.StatusBadRequest,
1✔
1103
                        l,
1✔
1104
                )
1✔
1105
                return
1✔
1106
        }
1✔
1107

1108
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
1✔
1109
        switch err {
1✔
1110
        default:
×
1111
                d.view.RenderInternalError(w, r, err, l)
×
1112
        case nil:
1✔
1113
                r.URL.Path = "./deployments"
1✔
1114
                d.view.RenderSuccessPost(w, r, id)
1✔
1115
        case app.ErrDuplicateDeployment:
1✔
1116
                d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1117
        case app.ErrInvalidDeploymentID:
1✔
1118
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1119
        }
1120
}
1121

1122
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1123
        r *rest.Request,
1124
        group string,
1125
) (*model.DeploymentConstructor, error) {
1✔
1126
        var constructor *model.DeploymentConstructor
1✔
1127
        if err := r.DecodeJsonPayload(&constructor); err != nil {
2✔
1128
                return nil, err
1✔
1129
        }
1✔
1130

1131
        constructor.Group = group
1✔
1132

1✔
1133
        if err := constructor.ValidateNew(); err != nil {
2✔
1134
                return nil, err
1✔
1135
        }
1✔
1136

1137
        return constructor, nil
1✔
1138
}
1139

1140
func (d *DeploymentsApiHandlers) GetDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1141
        ctx := r.Context()
1✔
1142
        l := requestlog.GetRequestLogger(r)
1✔
1143

1✔
1144
        id := r.PathParam("id")
1✔
1145

1✔
1146
        if !govalidator.IsUUID(id) {
2✔
1147
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
1148
                return
1✔
1149
        }
1✔
1150

1151
        deployment, err := d.app.GetDeployment(ctx, id)
1✔
1152
        if err != nil {
1✔
1153
                d.view.RenderInternalError(w, r, err, l)
×
1154
                return
×
1155
        }
×
1156

1157
        if deployment == nil {
1✔
1158
                d.view.RenderErrorNotFound(w, r, l)
×
1159
                return
×
1160
        }
×
1161

1162
        d.view.RenderSuccessGet(w, deployment)
1✔
1163
}
1164

1165
func (d *DeploymentsApiHandlers) GetDeploymentStats(w rest.ResponseWriter, r *rest.Request) {
1✔
1166
        ctx := r.Context()
1✔
1167
        l := requestlog.GetRequestLogger(r)
1✔
1168

1✔
1169
        id := r.PathParam("id")
1✔
1170

1✔
1171
        if !govalidator.IsUUID(id) {
1✔
1172
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1173
                return
×
1174
        }
×
1175

1176
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1177
        if err != nil {
1✔
1178
                d.view.RenderInternalError(w, r, err, l)
×
1179
                return
×
1180
        }
×
1181

1182
        if stats == nil {
1✔
1183
                d.view.RenderErrorNotFound(w, r, l)
×
1184
                return
×
1185
        }
×
1186

1187
        d.view.RenderSuccessGet(w, stats)
1✔
1188
}
1189

1190
func (d *DeploymentsApiHandlers) GetDeploymentsStats(w rest.ResponseWriter, r *rest.Request) {
×
1191

×
1192
        ctx := r.Context()
×
1193
        l := requestlog.GetRequestLogger(r)
×
1194

×
1195
        ids := model.DeploymentIDs{}
×
1196
        if err := r.DecodeJsonPayload(&ids); err != nil {
×
1197
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1198
                return
×
1199
        }
×
1200

1201
        if len(ids.IDs) == 0 {
×
1202
                w.WriteHeader(http.StatusOK)
×
1203
                _ = w.WriteJson(struct{}{})
×
1204
                return
×
1205
        }
×
1206

1207
        if err := ids.Validate(); err != nil {
×
1208
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1209
                return
×
1210
        }
×
1211

1212
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
×
1213
        if err != nil {
×
1214
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
×
1215
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1216
                        return
×
1217
                }
×
1218
                d.view.RenderInternalError(w, r, err, l)
×
1219
                return
×
1220
        }
1221

1222
        w.WriteHeader(http.StatusOK)
×
1223

×
1224
        _ = w.WriteJson(stats)
×
1225
}
1226

1227
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
1228
        ctx := r.Context()
×
1229
        l := requestlog.GetRequestLogger(r)
×
1230

×
1231
        id := r.PathParam("id")
×
1232

×
1233
        if !govalidator.IsUUID(id) {
×
1234
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1235
                return
×
1236
        }
×
1237

1238
        deployment, err := d.app.GetDeployment(ctx, id)
×
1239
        if err != nil {
×
1240
                d.view.RenderInternalError(w, r, err, l)
×
1241
                return
×
1242
        }
×
1243

1244
        if deployment == nil {
×
1245
                d.view.RenderErrorNotFound(w, r, l)
×
1246
                return
×
1247
        }
×
1248

1249
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1250
}
1251

1252
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1253
        ctx := r.Context()
1✔
1254
        l := requestlog.GetRequestLogger(r)
1✔
1255

1✔
1256
        id := r.PathParam("id")
1✔
1257

1✔
1258
        if !govalidator.IsUUID(id) {
1✔
1259
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1260
                return
×
1261
        }
×
1262

1263
        // receive request body
1264
        var status struct {
1✔
1265
                Status model.DeviceDeploymentStatus
1✔
1266
        }
1✔
1267

1✔
1268
        err := r.DecodeJsonPayload(&status)
1✔
1269
        if err != nil {
1✔
1270
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1271
                return
×
1272
        }
×
1273
        // "aborted" is the only supported status
1274
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1275
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
1276
        }
×
1277

1278
        l.Infof("Abort deployment: %s", id)
1✔
1279

1✔
1280
        // Check if deployment is finished
1✔
1281
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1282
        if err != nil {
1✔
1283
                d.view.RenderInternalError(w, r, err, l)
×
1284
                return
×
1285
        }
×
1286
        if isDeploymentFinished {
2✔
1287
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
1✔
1288
                return
1✔
1289
        }
1✔
1290

1291
        // Abort deployments for devices and update deployment stats
1292
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1293
                d.view.RenderInternalError(w, r, err, l)
×
1294
        }
×
1295

1296
        d.view.RenderEmptySuccessResponse(w)
1✔
1297
}
1298

1299
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1300
        var (
1✔
1301
                installed *model.InstalledDeviceDeployment
1✔
1302
                ctx       = r.Context()
1✔
1303
                l         = requestlog.GetRequestLogger(r)
1✔
1304
                idata     = identity.FromContext(ctx)
1✔
1305
        )
1✔
1306
        if idata == nil {
2✔
1307
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
1✔
1308
                return
1✔
1309
        }
1✔
1310

1311
        q := r.URL.Query()
1✔
1312
        defer func() {
2✔
1313
                var reEncode bool = false
1✔
1314
                if name := q.Get(ParamArtifactName); name != "" {
2✔
1315
                        q.Set(ParamArtifactName, Redacted)
1✔
1316
                        reEncode = true
1✔
1317
                }
1✔
1318
                if typ := q.Get(ParamDeviceType); typ != "" {
2✔
1319
                        q.Set(ParamDeviceType, Redacted)
1✔
1320
                        reEncode = true
1✔
1321
                }
1✔
1322
                if reEncode {
2✔
1323
                        r.URL.RawQuery = q.Encode()
1✔
1324
                }
1✔
1325
        }()
1326
        if strings.EqualFold(r.Method, http.MethodPost) {
1✔
1327
                // POST
×
1328
                installed = new(model.InstalledDeviceDeployment)
×
1329
                if err := r.DecodeJsonPayload(&installed); err != nil {
×
1330
                        d.view.RenderError(w, r,
×
1331
                                errors.Wrap(err, "invalid schema"),
×
1332
                                http.StatusBadRequest, l)
×
1333
                        return
×
1334
                }
×
1335
        } else {
1✔
1336
                // GET or HEAD
1✔
1337
                installed = &model.InstalledDeviceDeployment{
1✔
1338
                        ArtifactName: q.Get(ParamArtifactName),
1✔
1339
                        DeviceType:   q.Get(ParamDeviceType),
1✔
1340
                }
1✔
1341
        }
1✔
1342

1343
        if err := installed.Validate(); err != nil {
1✔
1344
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1345
                return
×
1346
        }
×
1347

1348
        request := &model.DeploymentNextRequest{
1✔
1349
                DeviceProvides: installed,
1✔
1350
        }
1✔
1351

1✔
1352
        d.getDeploymentForDevice(w, r, idata, request)
1✔
1353
}
1354

1355
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1356
        w rest.ResponseWriter,
1357
        r *rest.Request,
1358
        idata *identity.Identity,
1359
        request *model.DeploymentNextRequest,
1360
) {
1✔
1361
        ctx := r.Context()
1✔
1362
        l := requestlog.GetRequestLogger(r)
1✔
1363

1✔
1364
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
1✔
1365
        if err != nil {
2✔
1366
                if err == app.ErrConflictingRequestData {
2✔
1367
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1368
                } else {
1✔
1369
                        d.view.RenderInternalError(w, r, err, l)
×
1370
                }
×
1371
                return
1✔
1372
        }
1373

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

1412
        d.view.RenderSuccessGet(w, deployment)
1✔
1413
}
1414

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

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

1✔
1424
        idata := identity.FromContext(ctx)
1✔
1425
        if idata == nil {
1✔
1426
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1427
                return
×
1428
        }
×
1429

1430
        // receive request body
1431
        var report model.StatusReport
1✔
1432

1✔
1433
        err := r.DecodeJsonPayload(&report)
1✔
1434
        if err != nil {
1✔
1435
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1436
                return
×
1437
        }
×
1438

1439
        l.Infof("status: %+v", report)
1✔
1440
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
1✔
1441
                idata.Subject, model.DeviceDeploymentState{
1✔
1442
                        Status:   report.Status,
1✔
1443
                        SubState: report.SubState,
1✔
1444
                }); err != nil {
1✔
1445

×
1446
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
1447
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
1448
                } else if err == app.ErrStorageNotFound {
×
1449
                        d.view.RenderErrorNotFound(w, r, l)
×
1450
                } else {
×
1451
                        d.view.RenderInternalError(w, r, err, l)
×
1452
                }
×
1453
                return
×
1454
        }
1455

1456
        d.view.RenderEmptySuccessResponse(w)
1✔
1457
}
1458

1459
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1460
        w rest.ResponseWriter,
1461
        r *rest.Request,
1462
) {
1✔
1463
        ctx := r.Context()
1✔
1464
        l := requestlog.GetRequestLogger(r)
1✔
1465

1✔
1466
        did := r.PathParam("id")
1✔
1467

1✔
1468
        if !govalidator.IsUUID(did) {
1✔
1469
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1470
                return
×
1471
        }
×
1472

1473
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
1✔
1474
        if err != nil {
1✔
1475
                switch err {
×
1476
                case app.ErrModelDeploymentNotFound:
×
1477
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1478
                        return
×
1479
                default:
×
1480
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1481
                        return
×
1482
                }
1483
        }
1484

1485
        d.view.RenderSuccessGet(w, statuses)
1✔
1486
}
1487

1488
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1489
        w rest.ResponseWriter,
1490
        r *rest.Request,
1491
) {
1✔
1492
        ctx := r.Context()
1✔
1493
        l := requestlog.GetRequestLogger(r)
1✔
1494

1✔
1495
        did := r.PathParam("id")
1✔
1496

1✔
1497
        if !govalidator.IsUUID(did) {
1✔
1498
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1499
                return
×
1500
        }
×
1501

1502
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1503
        if err != nil {
1✔
1504
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1505
                return
×
1506
        }
×
1507

1508
        lq := store.ListQuery{
1✔
1509
                Skip:         int((page - 1) * perPage),
1✔
1510
                Limit:        int(perPage),
1✔
1511
                DeploymentID: did,
1✔
1512
        }
1✔
1513
        if status := r.URL.Query().Get("status"); status != "" {
1✔
1514
                lq.Status = &status
×
1515
        }
×
1516
        if err = lq.Validate(); err != nil {
1✔
1517
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1518
                return
×
1519
        }
×
1520

1521
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1522
        if err != nil {
1✔
1523
                switch err {
×
1524
                case app.ErrModelDeploymentNotFound:
×
1525
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1526
                        return
×
1527
                default:
×
1528
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1529
                        return
×
1530
                }
1531
        }
1532

1533
        hasNext := totalCount > int(page*perPage)
1✔
1534
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1535
        for _, l := range links {
2✔
1536
                w.Header().Add("Link", l)
1✔
1537
        }
1✔
1538
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1539
        d.view.RenderSuccessGet(w, statuses)
1✔
1540
}
1541

1542
func ParseLookupQuery(vals url.Values) (model.Query, error) {
1✔
1543
        query := model.Query{}
1✔
1544

1✔
1545
        search := vals.Get("search")
1✔
1546
        if search != "" {
1✔
1547
                query.SearchText = search
×
1548
        }
×
1549

1550
        createdBefore := vals.Get("created_before")
1✔
1551
        if createdBefore != "" {
1✔
1552
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
×
1553
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
×
1554
                } else {
×
1555
                        query.CreatedBefore = &createdBeforeTime
×
1556
                }
×
1557
        }
1558

1559
        createdAfter := vals.Get("created_after")
1✔
1560
        if createdAfter != "" {
1✔
1561
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1562
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1563
                } else {
×
1564
                        query.CreatedAfter = &createdAfterTime
×
1565
                }
×
1566
        }
1567

1568
        switch strings.ToLower(vals.Get("sort")) {
1✔
1569
        case model.SortDirectionAscending:
×
1570
                query.Sort = model.SortDirectionAscending
×
1571
        case "", model.SortDirectionDescending:
1✔
1572
                query.Sort = model.SortDirectionDescending
1✔
1573
        default:
×
1574
                return query, ErrInvalidSortDirection
×
1575
        }
1576

1577
        status := vals.Get("status")
1✔
1578
        switch status {
1✔
1579
        case "inprogress":
×
1580
                query.Status = model.StatusQueryInProgress
×
1581
        case "finished":
×
1582
                query.Status = model.StatusQueryFinished
×
1583
        case "pending":
×
1584
                query.Status = model.StatusQueryPending
×
1585
        case "aborted":
×
1586
                query.Status = model.StatusQueryAborted
×
1587
        case "":
1✔
1588
                query.Status = model.StatusQueryAny
1✔
1589
        default:
×
1590
                return query, errors.Errorf("unknown status %s", status)
×
1591

1592
        }
1593

1594
        dType := vals.Get("type")
1✔
1595
        if dType == "" {
2✔
1596
                return query, nil
1✔
1597
        }
1✔
1598
        deploymentType := model.DeploymentType(dType)
×
1599
        if deploymentType == model.DeploymentTypeSoftware ||
×
1600
                deploymentType == model.DeploymentTypeConfiguration {
×
1601
                query.Type = deploymentType
×
1602
        } else {
×
1603
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1604
        }
×
1605

1606
        return query, nil
×
1607
}
1608

1609
func parseEpochToTimestamp(epoch string) (time.Time, error) {
×
1610
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
×
1611
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
×
1612
        } else {
×
1613
                return time.Unix(epochInt64, 0).UTC(), nil
×
1614
        }
×
1615
}
1616

1617
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1618
        ctx := r.Context()
1✔
1619
        l := requestlog.GetRequestLogger(r)
1✔
1620
        q := r.URL.Query()
1✔
1621
        defer func() {
2✔
1622
                if search := q.Get("search"); search != "" {
1✔
1623
                        q.Set("search", Redacted)
×
1624
                        r.URL.RawQuery = q.Encode()
×
1625
                }
×
1626
        }()
1627

1628
        query, err := ParseLookupQuery(q)
1✔
1629
        if err != nil {
1✔
1630
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1631
                return
×
1632
        }
×
1633

1634
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1635
        if err != nil {
1✔
1636
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1637
                return
×
1638
        }
×
1639
        query.Skip = int((page - 1) * perPage)
1✔
1640
        query.Limit = int(perPage + 1)
1✔
1641

1✔
1642
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
1✔
1643
        if err != nil {
1✔
1644
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1645
                return
×
1646
        }
×
1647
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
1✔
1648

1✔
1649
        len := len(deps)
1✔
1650
        hasNext := false
1✔
1651
        if uint64(len) > perPage {
1✔
1652
                hasNext = true
×
1653
                len = int(perPage)
×
1654
        }
×
1655

1656
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1657
        for _, l := range links {
2✔
1658
                w.Header().Add("Link", l)
1✔
1659
        }
1✔
1660

1661
        d.view.RenderSuccessGet(w, deps[:len])
1✔
1662
}
1663

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

1✔
1668
        did := r.PathParam("id")
1✔
1669

1✔
1670
        idata := identity.FromContext(ctx)
1✔
1671
        if idata == nil {
1✔
1672
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1673
                return
×
1674
        }
×
1675

1676
        // reuse DeploymentLog, device and deployment IDs are ignored when
1677
        // (un-)marshaling DeploymentLog to/from JSON
1678
        var log model.DeploymentLog
1✔
1679

1✔
1680
        err := r.DecodeJsonPayload(&log)
1✔
1681
        if err != nil {
1✔
1682
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1683
                return
×
1684
        }
×
1685

1686
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1687
                did, log.Messages); err != nil {
1✔
1688

×
1689
                if err == app.ErrModelDeploymentNotFound {
×
1690
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1691
                } else {
×
1692
                        d.view.RenderInternalError(w, r, err, l)
×
1693
                }
×
1694
                return
×
1695
        }
1696

1697
        d.view.RenderEmptySuccessResponse(w)
1✔
1698
}
1699

1700
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1701
        ctx := r.Context()
1✔
1702
        l := requestlog.GetRequestLogger(r)
1✔
1703

1✔
1704
        did := r.PathParam("id")
1✔
1705
        devid := r.PathParam("devid")
1✔
1706

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

1✔
1709
        if err != nil {
1✔
1710
                d.view.RenderInternalError(w, r, err, l)
×
1711
                return
×
1712
        }
×
1713

1714
        if depl == nil {
1✔
1715
                d.view.RenderErrorNotFound(w, r, l)
×
1716
                return
×
1717
        }
×
1718

1719
        d.view.RenderDeploymentLog(w, *depl)
1✔
1720
}
1721

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

×
1726
        id := r.PathParam("id")
×
1727
        err := d.app.AbortDeviceDeployments(ctx, id)
×
1728

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

1737
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1738
        r *rest.Request) {
×
1739
        ctx := r.Context()
×
1740
        l := requestlog.GetRequestLogger(r)
×
1741

×
1742
        id := r.PathParam("id")
×
1743
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
×
1744

×
1745
        switch err {
×
1746
        case nil, app.ErrStorageNotFound:
×
1747
                d.view.RenderEmptySuccessResponse(w)
×
1748
        default:
×
1749
                d.view.RenderInternalError(w, r, err, l)
×
1750
        }
1751
}
1752

1753
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
×
1754
        ctx := r.Context()
×
1755
        d.listDeviceDeployments(ctx, w, r, true)
×
1756
}
×
1757

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

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

1784
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1785
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
×
1786
        l := requestlog.GetRequestLogger(r)
×
1787

×
1788
        did := ""
×
1789
        var IDs []string
×
1790
        if byDeviceID {
×
1791
                did = r.PathParam("id")
×
1792
        } else {
×
1793
                values := r.URL.Query()
×
1794
                if values.Has("id") && len(values["id"]) > 0 {
×
1795
                        IDs = values["id"]
×
1796
                } else {
×
1797
                        d.view.RenderError(w, r, ErrEmptyID, http.StatusBadRequest, l)
×
1798
                        return
×
1799
                }
×
1800
        }
1801

1802
        page, perPage, err := rest_utils.ParsePagination(r)
×
1803
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
×
1804
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
×
1805
        }
×
1806
        if err != nil {
×
1807
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1808
                return
×
1809
        }
×
1810

1811
        lq := store.ListQueryDeviceDeployments{
×
1812
                Skip:     int((page - 1) * perPage),
×
1813
                Limit:    int(perPage),
×
1814
                DeviceID: did,
×
1815
                IDs:      IDs,
×
1816
        }
×
1817
        if status := r.URL.Query().Get("status"); status != "" {
×
1818
                lq.Status = &status
×
1819
        }
×
1820
        if err = lq.Validate(); err != nil {
×
1821
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1822
                return
×
1823
        }
×
1824

1825
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
×
1826
        if err != nil {
×
1827
                d.view.RenderInternalError(w, r, err, l)
×
1828
                return
×
1829
        }
×
1830
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
×
1831

×
1832
        hasNext := totalCount > lq.Skip+len(deps)
×
1833
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
×
1834
        for _, l := range links {
×
1835
                w.Header().Add("Link", l)
×
1836
        }
×
1837

1838
        d.view.RenderSuccessGet(w, deps)
×
1839
}
1840

1841
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
1842
        r *rest.Request) {
×
1843
        ctx := r.Context()
×
1844
        tenantID := r.PathParam("tenantID")
×
1845
        if tenantID != "" {
×
1846
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1847
                        Tenant:   tenantID,
×
1848
                        IsDevice: true,
×
1849
                })
×
1850
        }
×
1851

1852
        l := requestlog.GetRequestLogger(r)
×
1853

×
1854
        id := r.PathParam("id")
×
1855

×
1856
        // Decommission deployments for devices and update deployment stats
×
1857
        err := d.app.DecommissionDevice(ctx, id)
×
1858

×
1859
        switch err {
×
1860
        case nil, app.ErrStorageNotFound:
×
1861
                d.view.RenderEmptySuccessResponse(w)
×
1862
        default:
×
1863
                d.view.RenderInternalError(w, r, err, l)
×
1864

1865
        }
1866
}
1867

1868
// tenants
1869

1870
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1871
        ctx := r.Context()
1✔
1872
        l := requestlog.GetRequestLogger(r)
1✔
1873

1✔
1874
        defer r.Body.Close()
1✔
1875

1✔
1876
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1877
        if err != nil {
2✔
1878
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1879
                return
1✔
1880
        }
1✔
1881

1882
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1883
        if err != nil {
1✔
1884
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1885
                return
×
1886
        }
×
1887

1888
        w.WriteHeader(http.StatusCreated)
1✔
1889
}
1890

1891
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1892
        w rest.ResponseWriter,
1893
        r *rest.Request,
1894
) {
×
1895
        tenantID := r.PathParam("tenant")
×
1896
        if tenantID == "" {
×
1897
                l := requestlog.GetRequestLogger(r)
×
1898
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
×
1899
                return
×
1900
        }
×
1901

1902
        r.Request = r.WithContext(identity.WithContext(
×
1903
                r.Context(),
×
1904
                &identity.Identity{Tenant: tenantID},
×
1905
        ))
×
1906
        d.LookupDeployment(w, r)
×
1907
}
1908

1909
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1910
        w rest.ResponseWriter,
1911
        r *rest.Request,
1912
) {
1✔
1913
        l := requestlog.GetRequestLogger(r)
1✔
1914

1✔
1915
        tenantID := r.PathParam("tenant")
1✔
1916

1✔
1917
        ctx := identity.WithContext(
1✔
1918
                r.Context(),
1✔
1919
                &identity.Identity{Tenant: tenantID},
1✔
1920
        )
1✔
1921

1✔
1922
        settings, err := d.app.GetStorageSettings(ctx)
1✔
1923
        if err != nil {
1✔
1924
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1925
                return
×
1926
        }
×
1927

1928
        d.view.RenderSuccessGet(w, settings)
1✔
1929
}
1930

1931
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1932
        w rest.ResponseWriter,
1933
        r *rest.Request,
1934
) {
1✔
1935
        l := requestlog.GetRequestLogger(r)
1✔
1936

1✔
1937
        defer r.Body.Close()
1✔
1938

1✔
1939
        tenantID := r.PathParam("tenant")
1✔
1940

1✔
1941
        ctx := identity.WithContext(
1✔
1942
                r.Context(),
1✔
1943
                &identity.Identity{Tenant: tenantID},
1✔
1944
        )
1✔
1945

1✔
1946
        settings, err := model.ParseStorageSettingsRequest(r.Body)
1✔
1947
        if err != nil {
2✔
1948
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1949
                return
1✔
1950
        }
1✔
1951

1952
        err = d.app.SetStorageSettings(ctx, settings)
1✔
1953
        if err != nil {
1✔
1954
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1955
                return
×
1956
        }
×
1957

1958
        w.WriteHeader(http.StatusNoContent)
1✔
1959
}
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