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

mendersoftware / mender-server / 1931958122

17 Jul 2025 09:44AM UTC coverage: 65.472% (-0.05%) from 65.521%
1931958122

Pull #805

gitlab-ci

alfrunes
test: Fix tests making invalid assertions for request body

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #805: Revert to absolute API paths and group router by middleware

82 of 91 new or added lines in 3 files covered. (90.11%)

5 existing lines in 3 files now uncovered.

32139 of 49088 relevant lines covered (65.47%)

1.39 hits per line

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

79.27
/backend/services/deployments/api/http/api_deployments.go
1
// Copyright 2024 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/asaskevich/govalidator"
30
        "github.com/gin-gonic/gin"
31
        "github.com/pkg/errors"
32

33
        "github.com/mendersoftware/mender-server/pkg/config"
34
        "github.com/mendersoftware/mender-server/pkg/identity"
35
        "github.com/mendersoftware/mender-server/pkg/log"
36
        "github.com/mendersoftware/mender-server/pkg/requestid"
37
        "github.com/mendersoftware/mender-server/pkg/rest.utils"
38

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

46
const (
47
        // 15 minutes
48
        DefaultDownloadLinkExpire = 15 * time.Minute
49
        // 10 Mb
50
        MaxFormParamSize           = 1024 * 1024             // 1MiB
51
        DefaultMaxImageSize        = 10 * 1024 * 1024 * 1024 // 10GiB
52
        DefaultMaxGenerateDataSize = 512 * 1024 * 1024       // 512MiB
53

54
        // Pagination
55
        DefaultPerPage                      = 20
56
        MaximumPerPage                      = 500
57
        MaximumPerPageListDeviceDeployments = 20
58
)
59

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

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

85
const Redacted = "REDACTED"
86

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

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

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

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

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

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

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

141
        // RequestSize is the maximum request body size
142
        MaxRequestSize int64
143

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

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

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

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

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

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

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

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

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

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

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

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

209
func (conf *Config) SetMaxRequestSize(size int64) *Config {
2✔
210
        conf.MaxRequestSize = size
2✔
211
        return conf
2✔
212
}
2✔
213

214
type DeploymentsApiHandlers struct {
215
        view   RESTView
216
        store  store.DataStore
217
        app    app.App
218
        config Config
219
}
220

221
func NewDeploymentsApiHandlers(
222
        store store.DataStore,
223
        view RESTView,
224
        app app.App,
225
        config ...*Config,
226
) *DeploymentsApiHandlers {
3✔
227
        conf := NewConfig()
3✔
228
        for _, c := range config {
6✔
229
                if c == nil {
3✔
UNCOV
230
                        continue
×
231
                }
232
                if c.PresignSecret != nil {
6✔
233
                        conf.PresignSecret = c.PresignSecret
3✔
234
                }
3✔
235
                if c.PresignExpire != 0 {
6✔
236
                        conf.PresignExpire = c.PresignExpire
3✔
237
                }
3✔
238
                if c.PresignHostname != "" {
6✔
239
                        conf.PresignHostname = c.PresignHostname
3✔
240
                }
3✔
241
                if c.PresignScheme != "" {
6✔
242
                        conf.PresignScheme = c.PresignScheme
3✔
243
                }
3✔
244
                if c.MaxImageSize > 0 {
6✔
245
                        conf.MaxImageSize = c.MaxImageSize
3✔
246
                }
3✔
247
                if c.MaxGenerateDataSize > 0 {
6✔
248
                        conf.MaxGenerateDataSize = c.MaxGenerateDataSize
3✔
249
                }
3✔
250
                if c.MaxRequestSize > 0 {
6✔
251
                        conf.MaxRequestSize = c.MaxRequestSize
3✔
252
                }
3✔
253
                conf.DisableNewReleasesFeature = c.DisableNewReleasesFeature
3✔
254
                conf.EnableDirectUpload = c.EnableDirectUpload
3✔
255
                conf.EnableDirectUploadSkipVerify = c.EnableDirectUploadSkipVerify
3✔
256
        }
257
        return &DeploymentsApiHandlers{
3✔
258
                store:  store,
3✔
259
                view:   view,
3✔
260
                app:    app,
3✔
261
                config: *conf,
3✔
262
        }
3✔
263
}
264

265
func (d *DeploymentsApiHandlers) AliveHandler(c *gin.Context) {
2✔
266
        c.Status(http.StatusNoContent)
2✔
267
}
2✔
268

269
func (d *DeploymentsApiHandlers) HealthHandler(c *gin.Context) {
2✔
270
        ctx := c.Request.Context()
2✔
271
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
2✔
272
        defer cancel()
2✔
273

2✔
274
        err := d.app.HealthCheck(ctx)
2✔
275
        if err != nil {
3✔
276
                d.view.RenderError(c, err, http.StatusServiceUnavailable)
1✔
277
                return
1✔
278
        }
1✔
279
        c.Status(http.StatusNoContent)
2✔
280
}
281

282
func getReleaseOrImageFilter(r *http.Request, version listReleasesVersion,
283
        paginated bool) *model.ReleaseOrImageFilter {
3✔
284

3✔
285
        q := r.URL.Query()
3✔
286

3✔
287
        filter := &model.ReleaseOrImageFilter{
3✔
288
                Name:       q.Get(ParamName),
3✔
289
                UpdateType: q.Get(ParamUpdateType),
3✔
290
        }
3✔
291
        if version == listReleasesV1 {
6✔
292
                filter.Description = q.Get(ParamDescription)
3✔
293
                filter.DeviceType = q.Get(ParamDeviceType)
3✔
294
        } else if version == listReleasesV2 {
5✔
295
                filter.Tags = q[ParamTag]
1✔
296
                for i, t := range filter.Tags {
2✔
297
                        filter.Tags[i] = strings.ToLower(t)
1✔
298
                }
1✔
299
        }
300

301
        if paginated {
5✔
302
                filter.Sort = q.Get(ParamSort)
2✔
303
                if page := q.Get(ParamPage); page != "" {
3✔
304
                        if i, err := strconv.Atoi(page); err == nil {
2✔
305
                                filter.Page = i
1✔
306
                        }
1✔
307
                }
308
                if perPage := q.Get(ParamPerPage); perPage != "" {
3✔
309
                        if i, err := strconv.Atoi(perPage); err == nil {
2✔
310
                                filter.PerPage = i
1✔
311
                        }
1✔
312
                }
313
                if filter.Page <= 0 {
4✔
314
                        filter.Page = 1
2✔
315
                }
2✔
316
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
4✔
317
                        filter.PerPage = DefaultPerPage
2✔
318
                }
2✔
319
        }
320

321
        return filter
3✔
322
}
323

324
type limitResponse struct {
325
        Limit uint64 `json:"limit"`
326
        Usage uint64 `json:"usage"`
327
}
328

329
func (d *DeploymentsApiHandlers) GetLimit(c *gin.Context) {
1✔
330

1✔
331
        name := c.Param("name")
1✔
332

1✔
333
        if !model.IsValidLimit(name) {
2✔
334
                d.view.RenderError(c,
1✔
335
                        errors.Errorf("unsupported limit %s", name),
1✔
336
                        http.StatusBadRequest)
1✔
337
                return
1✔
338
        }
1✔
339

340
        limit, err := d.app.GetLimit(c.Request.Context(), name)
1✔
341
        if err != nil {
2✔
342
                d.view.RenderInternalError(c, err)
1✔
343
                return
1✔
344
        }
1✔
345

346
        d.view.RenderSuccessGet(c, limitResponse{
1✔
347
                Limit: limit.Value,
1✔
348
                Usage: 0, // TODO fill this when ready
1✔
349
        })
1✔
350
}
351

352
// images
353

354
func (d *DeploymentsApiHandlers) GetImage(c *gin.Context) {
2✔
355

2✔
356
        id := c.Param("id")
2✔
357

2✔
358
        if !govalidator.IsUUID(id) {
3✔
359
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
360
                return
1✔
361
        }
1✔
362

363
        image, err := d.app.GetImage(c.Request.Context(), id)
2✔
364
        if err != nil {
2✔
365
                d.view.RenderInternalError(c, err)
×
366
                return
×
367
        }
×
368

369
        if image == nil {
3✔
370
                d.view.RenderErrorNotFound(c)
1✔
371
                return
1✔
372
        }
1✔
373

374
        d.view.RenderSuccessGet(c, image)
2✔
375
}
376

377
func (d *DeploymentsApiHandlers) GetImages(c *gin.Context) {
3✔
378

3✔
379
        defer redactReleaseName(c.Request)
3✔
380
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, false)
3✔
381

3✔
382
        list, _, err := d.app.ListImages(c.Request.Context(), filter)
3✔
383
        if err != nil {
4✔
384
                d.view.RenderInternalError(c, err)
1✔
385
                return
1✔
386
        }
1✔
387

388
        d.view.RenderSuccessGet(c, list)
3✔
389
}
390

391
func (d *DeploymentsApiHandlers) ListImages(c *gin.Context) {
1✔
392

1✔
393
        defer redactReleaseName(c.Request)
1✔
394
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, true)
1✔
395

1✔
396
        list, totalCount, err := d.app.ListImages(c.Request.Context(), filter)
1✔
397
        if err != nil {
2✔
398
                d.view.RenderInternalError(c, err)
1✔
399
                return
1✔
400
        }
1✔
401

402
        hasNext := totalCount > int(filter.Page*filter.PerPage)
1✔
403

1✔
404
        hints := rest.NewPagingHints().
1✔
405
                SetPage(int64(filter.Page)).
1✔
406
                SetPerPage(int64(filter.PerPage)).
1✔
407
                SetHasNext(hasNext).
1✔
408
                SetTotalCount(int64(totalCount))
1✔
409

1✔
410
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
411
        if err != nil {
1✔
412
                d.view.RenderInternalError(c, err)
×
413
                return
×
414
        }
×
415

416
        for _, l := range links {
2✔
417
                c.Writer.Header().Add(hdrLink, l)
1✔
418
        }
1✔
419
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
420

1✔
421
        d.view.RenderSuccessGet(c, list)
1✔
422
}
423

424
func (d *DeploymentsApiHandlers) DownloadLink(c *gin.Context) {
1✔
425

1✔
426
        id := c.Param("id")
1✔
427

1✔
428
        if !govalidator.IsUUID(id) {
1✔
429
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
430
                return
×
431
        }
×
432

433
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
434
        link, err := d.app.DownloadLink(c.Request.Context(), id,
1✔
435
                time.Duration(expireSeconds)*time.Second)
1✔
436
        if err != nil {
1✔
437
                d.view.RenderInternalError(c, err)
×
438
                return
×
439
        }
×
440

441
        if link == nil {
1✔
442
                d.view.RenderErrorNotFound(c)
×
443
                return
×
444
        }
×
445

446
        d.view.RenderSuccessGet(c, link)
1✔
447
}
448

449
func (d *DeploymentsApiHandlers) UploadLink(c *gin.Context) {
2✔
450

2✔
451
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
2✔
452
        link, err := d.app.UploadLink(
2✔
453
                c.Request.Context(),
2✔
454
                time.Duration(expireSeconds)*time.Second,
2✔
455
                d.config.EnableDirectUploadSkipVerify,
2✔
456
        )
2✔
457
        if err != nil {
3✔
458
                d.view.RenderInternalError(c, err)
1✔
459
                return
1✔
460
        }
1✔
461

462
        if link == nil {
3✔
463
                d.view.RenderErrorNotFound(c)
1✔
464
                return
1✔
465
        }
1✔
466

467
        d.view.RenderSuccessGet(c, link)
2✔
468
}
469

470
const maxMetadataSize = 2048
471

472
func (d *DeploymentsApiHandlers) CompleteUpload(c *gin.Context) {
2✔
473
        ctx := c.Request.Context()
2✔
474
        l := log.FromContext(ctx)
2✔
475

2✔
476
        artifactID := c.Param(ParamID)
2✔
477

2✔
478
        var metadata *model.DirectUploadMetadata
2✔
479
        if d.config.EnableDirectUploadSkipVerify {
3✔
480
                var directMetadata model.DirectUploadMetadata
1✔
481
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
482
                n, err := io.ReadFull(c.Request.Body, bodyBuffer)
1✔
483
                c.Request.Body.Close()
1✔
484
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
485
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
486
                } else {
1✔
487
                        err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
488
                        if err == nil {
2✔
489
                                if directMetadata.Validate() == nil {
2✔
490
                                        metadata = &directMetadata
1✔
491
                                }
1✔
492
                        } else {
1✔
493
                                l.Errorf("error parsing json data: %s", err.Error())
1✔
494
                        }
1✔
495
                }
496
        }
497

498
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
2✔
499
        switch errors.Cause(err) {
2✔
500
        case nil:
2✔
501
                // c.Writer.Header().Set("Link", "FEAT: Upload status API")
2✔
502
                c.Status(http.StatusAccepted)
2✔
503
        case app.ErrUploadNotFound:
1✔
504
                d.view.RenderErrorNotFound(c)
1✔
505
        default:
1✔
506
                d.view.RenderInternalError(c, err)
1✔
507
        }
508
}
509

510
func (d *DeploymentsApiHandlers) DownloadConfiguration(c *gin.Context) {
3✔
511
        if d.config.PresignSecret == nil {
4✔
512
                d.view.RenderErrorNotFound(c)
1✔
513
                return
1✔
514
        }
1✔
515
        var (
3✔
516
                deviceID, _     = url.PathUnescape(c.Param(ParamDeviceID))
3✔
517
                deviceType, _   = url.PathUnescape(c.Param(ParamDeviceType))
3✔
518
                deploymentID, _ = url.PathUnescape(c.Param(ParamDeploymentID))
3✔
519
        )
3✔
520
        if deviceID == "" || deviceType == "" || deploymentID == "" {
3✔
521
                d.view.RenderErrorNotFound(c)
×
522
                return
×
523
        }
×
524

525
        var (
3✔
526
                tenantID string
3✔
527
                q        = c.Request.URL.Query()
3✔
528
                err      error
3✔
529
        )
3✔
530
        tenantID = q.Get(ParamTenantID)
3✔
531
        sig := model.NewRequestSignature(c.Request, d.config.PresignSecret)
3✔
532
        if err = sig.Validate(); err != nil {
6✔
533
                switch cause := errors.Cause(err); cause {
3✔
534
                case model.ErrLinkExpired:
1✔
535
                        d.view.RenderError(c, cause, http.StatusForbidden)
1✔
536
                default:
3✔
537
                        d.view.RenderError(c,
3✔
538
                                errors.Wrap(err, "invalid request parameters"),
3✔
539
                                http.StatusBadRequest,
3✔
540
                        )
3✔
541
                }
542
                return
3✔
543
        }
544

545
        if !sig.VerifyHMAC256() {
4✔
546
                d.view.RenderError(c,
2✔
547
                        errors.New("signature invalid"),
2✔
548
                        http.StatusForbidden,
2✔
549
                )
2✔
550
                return
2✔
551
        }
2✔
552

553
        // Validate request signature
554
        ctx := identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
555
                Subject:  deviceID,
2✔
556
                Tenant:   tenantID,
2✔
557
                IsDevice: true,
2✔
558
        })
2✔
559

2✔
560
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
2✔
561
        if err != nil {
3✔
562
                switch cause := errors.Cause(err); cause {
1✔
563
                case app.ErrModelDeploymentNotFound:
1✔
564
                        d.view.RenderError(c,
1✔
565
                                errors.Errorf(
1✔
566
                                        "deployment with id '%s' not found",
1✔
567
                                        deploymentID,
1✔
568
                                ),
1✔
569
                                http.StatusNotFound,
1✔
570
                        )
1✔
571
                default:
1✔
572
                        d.view.RenderInternalError(c, err)
1✔
573
                }
574
                return
1✔
575
        }
576
        artifactPayload, err := io.ReadAll(artifact)
2✔
577
        if err != nil {
3✔
578
                d.view.RenderInternalError(c, err)
1✔
579
                return
1✔
580
        }
1✔
581

582
        rw := c.Writer
2✔
583
        hdr := rw.Header()
2✔
584
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
2✔
585
        hdr.Set("Content-Type", app.ArtifactContentType)
2✔
586
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
2✔
587
        c.Status(http.StatusOK)
2✔
588
        _, err = rw.Write(artifactPayload)
2✔
589
        if err != nil {
2✔
590
                // There's not anything we can do here in terms of the response.
×
591
                _ = c.Error(err)
×
592
        }
×
593
}
594

595
func (d *DeploymentsApiHandlers) DeleteImage(c *gin.Context) {
1✔
596

1✔
597
        id := c.Param("id")
1✔
598

1✔
599
        if !govalidator.IsUUID(id) {
1✔
600
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
601
                return
×
602
        }
×
603

604
        if err := d.app.DeleteImage(c.Request.Context(), id); err != nil {
2✔
605
                switch err {
1✔
606
                default:
×
607
                        d.view.RenderInternalError(c, err)
×
608
                case app.ErrImageMetaNotFound:
×
609
                        d.view.RenderErrorNotFound(c)
×
610
                case app.ErrModelImageInActiveDeployment:
1✔
611
                        d.view.RenderError(c, ErrArtifactUsedInActiveDeployment, http.StatusConflict)
1✔
612
                }
613
                return
1✔
614
        }
615

616
        d.view.RenderSuccessDelete(c)
1✔
617
}
618

619
func (d *DeploymentsApiHandlers) EditImage(c *gin.Context) {
×
620

×
621
        id := c.Param("id")
×
622

×
623
        if !govalidator.IsUUID(id) {
×
624
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
625
                return
×
626
        }
×
627

628
        constructor, err := getImageMetaFromBody(c)
×
629
        if err != nil {
×
630
                d.view.RenderError(
×
631
                        c,
×
632
                        errors.Wrap(err, "Validating request body"),
×
633
                        http.StatusBadRequest,
×
634
                )
×
635
                return
×
636
        }
×
637

638
        found, err := d.app.EditImage(c.Request.Context(), id, constructor)
×
639
        if err != nil {
×
640
                if err == app.ErrModelImageUsedInAnyDeployment {
×
641
                        d.view.RenderError(c, err, http.StatusUnprocessableEntity)
×
642
                        return
×
643
                }
×
644
                d.view.RenderInternalError(c, err)
×
645
                return
×
646
        }
647

648
        if !found {
×
649
                d.view.RenderErrorNotFound(c)
×
650
                return
×
651
        }
×
652

653
        d.view.RenderSuccessPut(c)
×
654
}
655

656
func getImageMetaFromBody(c *gin.Context) (*model.ImageMeta, error) {
×
657

×
658
        var constructor *model.ImageMeta
×
659

×
660
        if err := c.ShouldBindJSON(&constructor); err != nil {
×
661
                return nil, err
×
662
        }
×
663

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

668
        return constructor, nil
×
669
}
670

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

679
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(c *gin.Context) {
3✔
680

3✔
681
        tenantID := c.Param("tenant")
3✔
682

3✔
683
        if tenantID == "" {
3✔
684
                rest.RenderError(
×
685
                        c,
×
686
                        http.StatusBadRequest,
×
687
                        fmt.Errorf("missing tenant id in path"),
×
688
                )
×
689
                return
×
690
        }
×
691

692
        var ctx context.Context
3✔
693
        if tenantID != "default" {
5✔
694
                ident := &identity.Identity{Tenant: tenantID}
2✔
695
                ctx = identity.WithContext(c.Request.Context(), ident)
2✔
696
        } else {
4✔
697
                ctx = c.Request.Context()
2✔
698
        }
2✔
699

700
        d.newImageWithContext(ctx, c)
3✔
701
}
702

703
func (d *DeploymentsApiHandlers) newImageWithContext(
704
        ctx context.Context,
705
        c *gin.Context,
706
) {
3✔
707

3✔
708
        formReader, err := c.Request.MultipartReader()
3✔
709
        if err != nil {
5✔
710
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
711
                return
2✔
712
        }
2✔
713

714
        // parse multipart message
715
        multipartUploadMsg, err := d.ParseMultipart(formReader)
3✔
716

3✔
717
        if err != nil {
5✔
718
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
719
                return
2✔
720
        }
2✔
721

722
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
3✔
723
        if err == nil {
6✔
724
                d.view.RenderSuccessPost(c, imgID)
3✔
725
                return
3✔
726
        }
3✔
727
        var cErr *model.ConflictError
2✔
728
        if errors.As(err, &cErr) {
3✔
729
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
1✔
730
                c.JSON(http.StatusConflict, cErr)
1✔
731
                return
1✔
732
        }
1✔
733
        cause := errors.Cause(err)
1✔
734
        switch cause {
1✔
735
        default:
×
736
                d.view.RenderInternalError(c, err)
×
737
                return
×
738
        case app.ErrModelArtifactNotUnique:
×
739
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
×
740
                return
×
741
        case app.ErrModelParsingArtifactFailed:
1✔
742
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
743
                return
1✔
744
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
745
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
×
746
                return
×
747
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
748
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
749
                io.ErrUnexpectedEOF:
×
750
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
751
                return
×
752
        }
753
}
754

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

2✔
759
        // handle specific cases
2✔
760

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

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

770
        return errors.New(errMsg)
2✔
771
}
772

773
// GenerateImage s the multipart Raw Data/Meta upload handler.
774
// Request should be of type "multipart/form-data". The parts are
775
// key/valyue pairs of metadata information except the last one,
776
// which must contain the file containing the raw data to be processed
777
// into an artifact.
778
func (d *DeploymentsApiHandlers) GenerateImage(c *gin.Context) {
3✔
779

3✔
780
        formReader, err := c.Request.MultipartReader()
3✔
781
        if err != nil {
4✔
782
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
783
                return
1✔
784
        }
1✔
785

786
        // parse multipart message
787
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
3✔
788
        if err != nil {
4✔
789
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
790
                return
1✔
791
        }
1✔
792

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

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

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

850
                case "size":
3✔
851
                        // Add size limit to the metadata
3✔
852
                        reader := utils.ReadAtMost(part, 20)
3✔
853
                        sz, err := io.ReadAll(reader)
3✔
854
                        if err != nil {
4✔
855
                                return nil, errors.Wrap(err,
1✔
856
                                        "failed to read form value 'size'",
1✔
857
                                )
1✔
858
                        }
1✔
859
                        size, err = strconv.ParseInt(string(sz), 10, 64)
3✔
860
                        if err != nil {
3✔
861
                                return nil, err
×
862
                        }
×
863
                        if size > d.config.MaxImageSize {
3✔
864
                                return nil, ErrModelArtifactFileTooLarge
×
865
                        }
×
866

867
                case "artifact_id":
3✔
868
                        // Add artifact id to the metadata (must be a valid UUID).
3✔
869
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
870
                        b, err := io.ReadAll(reader)
3✔
871
                        if err != nil {
3✔
872
                                return nil, errors.Wrap(err,
×
873
                                        "failed to read form value 'artifact_id'",
×
874
                                )
×
875
                        }
×
876
                        id := string(b)
3✔
877
                        if !govalidator.IsUUID(id) {
5✔
878
                                return nil, errors.New(
2✔
879
                                        "artifact_id is not a valid UUID",
2✔
880
                                )
2✔
881
                        }
2✔
882
                        uploadMsg.ArtifactID = id
2✔
883

884
                case "artifact":
3✔
885
                        // Assign the form-data payload to the artifact reader
3✔
886
                        // and return. The content is consumed elsewhere.
3✔
887
                        if size > 0 {
6✔
888
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
3✔
889
                        } else {
4✔
890
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
1✔
891
                                        part,
1✔
892
                                        d.config.MaxImageSize,
1✔
893
                                )
1✔
894
                        }
1✔
895
                        return uploadMsg, nil
3✔
896

897
                default:
2✔
898
                        // Ignore all non-API sections.
2✔
899
                        continue
2✔
900
                }
901
        }
902
}
903

904
// ParseGenerateImageMultipart parses multipart/form-data message.
905
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
906
        r *multipart.Reader,
907
) (*model.MultipartGenerateImageMsg, error) {
3✔
908
        msg := &model.MultipartGenerateImageMsg{}
3✔
909
        var size int64
3✔
910

3✔
911
ParseLoop:
3✔
912
        for {
6✔
913
                part, err := r.NextPart()
3✔
914
                if err != nil {
4✔
915
                        if err == io.EOF {
2✔
916
                                break
1✔
917
                        }
918
                        return nil, err
×
919
                }
920
                switch strings.ToLower(part.FormName()) {
3✔
921
                case "args":
3✔
922
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
923
                        b, err := io.ReadAll(reader)
3✔
924
                        if err != nil {
3✔
925
                                return nil, errors.Wrap(err,
×
926
                                        "failed to read form value 'args'",
×
927
                                )
×
928
                        }
×
929
                        msg.Args = string(b)
3✔
930

931
                case "description":
3✔
932
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
933
                        b, err := io.ReadAll(reader)
3✔
934
                        if err != nil {
3✔
935
                                return nil, errors.Wrap(err,
×
936
                                        "failed to read form value 'description'",
×
937
                                )
×
938
                        }
×
939
                        msg.Description = string(b)
3✔
940

941
                case "device_types_compatible":
3✔
942
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
943
                        b, err := io.ReadAll(reader)
3✔
944
                        if err != nil {
3✔
945
                                return nil, errors.Wrap(err,
×
946
                                        "failed to read form value 'device_types_compatible'",
×
947
                                )
×
948
                        }
×
949
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
3✔
950

951
                case "file":
3✔
952
                        if size > 0 {
4✔
953
                                msg.FileReader = utils.ReadExactly(part, size)
1✔
954
                        } else {
4✔
955
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxGenerateDataSize)
3✔
956
                        }
3✔
957
                        break ParseLoop
3✔
958

959
                case "name":
3✔
960
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
961
                        b, err := io.ReadAll(reader)
3✔
962
                        if err != nil {
3✔
963
                                return nil, errors.Wrap(err,
×
964
                                        "failed to read form value 'name'",
×
965
                                )
×
966
                        }
×
967
                        msg.Name = string(b)
3✔
968

969
                case "type":
3✔
970
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
971
                        b, err := io.ReadAll(reader)
3✔
972
                        if err != nil {
3✔
973
                                return nil, errors.Wrap(err,
×
974
                                        "failed to read form value 'type'",
×
975
                                )
×
976
                        }
×
977
                        msg.Type = string(b)
3✔
978

979
                case "size":
1✔
980
                        // Add size limit to the metadata
1✔
981
                        reader := utils.ReadAtMost(part, 20)
1✔
982
                        sz, err := io.ReadAll(reader)
1✔
983
                        if err != nil {
2✔
984
                                return nil, errors.Wrap(err,
1✔
985
                                        "failed to read form value 'size'",
1✔
986
                                )
1✔
987
                        }
1✔
988
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
989
                        if err != nil {
1✔
990
                                return nil, err
×
991
                        }
×
992
                        if size > d.config.MaxGenerateDataSize {
1✔
993
                                return nil, ErrModelArtifactFileTooLarge
×
994
                        }
×
995

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

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

1005
// deployments
1006
func (d *DeploymentsApiHandlers) createDeployment(
1007
        c *gin.Context,
1008
        ctx context.Context,
1009
        group string,
1010
) {
3✔
1011
        constructor, err := d.getDeploymentConstructorFromBody(c, group)
3✔
1012
        if err != nil {
6✔
1013
                d.view.RenderError(
3✔
1014
                        c,
3✔
1015
                        errors.Wrap(err, "Validating request body"),
3✔
1016
                        http.StatusBadRequest,
3✔
1017
                )
3✔
1018
                return
3✔
1019
        }
3✔
1020

1021
        id, err := d.app.CreateDeployment(ctx, constructor)
3✔
1022
        switch err {
3✔
1023
        case nil:
3✔
1024
                location := fmt.Sprintf("%s/%s", ApiUrlManagementDeployments, id)
3✔
1025
                c.Writer.Header().Add("Location", location)
3✔
1026
                c.Status(http.StatusCreated)
3✔
1027
        case app.ErrNoArtifact:
1✔
1028
                d.view.RenderError(c, err, http.StatusUnprocessableEntity)
1✔
1029
        case app.ErrNoDevices:
1✔
1030
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1031
        case app.ErrConflictingDeployment:
2✔
1032
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1033
        default:
1✔
1034
                d.view.RenderInternalError(c, err)
1✔
1035
        }
1036
}
1037

1038
func (d *DeploymentsApiHandlers) PostDeployment(c *gin.Context) {
3✔
1039
        ctx := c.Request.Context()
3✔
1040

3✔
1041
        d.createDeployment(c, ctx, "")
3✔
1042
}
3✔
1043

1044
func (d *DeploymentsApiHandlers) DeployToGroup(c *gin.Context) {
2✔
1045
        ctx := c.Request.Context()
2✔
1046

2✔
1047
        group := c.Param("name")
2✔
1048
        if len(group) < 1 {
2✔
1049
                d.view.RenderError(c, ErrMissingGroupName, http.StatusBadRequest)
×
1050
        }
×
1051
        d.createDeployment(c, ctx, group)
2✔
1052
}
1053

1054
// parseDeviceConfigurationDeploymentPathParams parses expected params
1055
// and check if the params are not empty
1056
func parseDeviceConfigurationDeploymentPathParams(c *gin.Context) (string, string, string, error) {
3✔
1057
        tenantID := c.Param("tenant")
3✔
1058
        deviceID := c.Param(ParamDeviceID)
3✔
1059
        if deviceID == "" {
3✔
1060
                return "", "", "", errors.New("device ID missing")
×
1061
        }
×
1062
        deploymentID := c.Param(ParamDeploymentID)
3✔
1063
        if deploymentID == "" {
3✔
1064
                return "", "", "", errors.New("deployment ID missing")
×
1065
        }
×
1066
        return tenantID, deviceID, deploymentID, nil
3✔
1067
}
1068

1069
// getConfigurationDeploymentConstructorFromBody extracts configuration
1070
// deployment constructor from the request body and validates it
1071
func getConfigurationDeploymentConstructorFromBody(c *gin.Context) (
1072
        *model.ConfigurationDeploymentConstructor, error) {
3✔
1073

3✔
1074
        var constructor *model.ConfigurationDeploymentConstructor
3✔
1075

3✔
1076
        if err := c.ShouldBindJSON(&constructor); err != nil {
5✔
1077
                return nil, err
2✔
1078
        }
2✔
1079

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

1084
        return constructor, nil
2✔
1085
}
1086

1087
// device configuration deployment handler
1088
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1089
        c *gin.Context,
1090
) {
3✔
1091

3✔
1092
        // get path params
3✔
1093
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(c)
3✔
1094
        if err != nil {
3✔
1095
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1096
                return
×
1097
        }
×
1098

1099
        // add tenant id to the context
1100
        ctx := identity.WithContext(c.Request.Context(), &identity.Identity{Tenant: tenantID})
3✔
1101

3✔
1102
        constructor, err := getConfigurationDeploymentConstructorFromBody(c)
3✔
1103
        if err != nil {
6✔
1104
                d.view.RenderError(
3✔
1105
                        c,
3✔
1106
                        errors.Wrap(err, "Validating request body"),
3✔
1107
                        http.StatusBadRequest,
3✔
1108
                )
3✔
1109
                return
3✔
1110
        }
3✔
1111

1112
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
2✔
1113
        switch err {
2✔
1114
        default:
1✔
1115
                d.view.RenderInternalError(c, err)
1✔
1116
        case nil:
2✔
1117
                c.Request.URL.Path = "./deployments"
2✔
1118
                d.view.RenderSuccessPost(c, id)
2✔
1119
        case app.ErrDuplicateDeployment:
2✔
1120
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1121
        case app.ErrInvalidDeploymentID:
1✔
1122
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1123
        }
1124
}
1125

1126
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1127
        c *gin.Context,
1128
        group string,
1129
) (*model.DeploymentConstructor, error) {
3✔
1130
        var constructor *model.DeploymentConstructor
3✔
1131
        if err := c.ShouldBindJSON(&constructor); err != nil {
4✔
1132
                return nil, err
1✔
1133
        }
1✔
1134

1135
        constructor.Group = group
3✔
1136

3✔
1137
        if err := constructor.ValidateNew(); err != nil {
6✔
1138
                return nil, err
3✔
1139
        }
3✔
1140

1141
        return constructor, nil
3✔
1142
}
1143

1144
func (d *DeploymentsApiHandlers) GetDeployment(c *gin.Context) {
2✔
1145
        ctx := c.Request.Context()
2✔
1146

2✔
1147
        id := c.Param("id")
2✔
1148

2✔
1149
        if !govalidator.IsUUID(id) {
3✔
1150
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
1151
                return
1✔
1152
        }
1✔
1153

1154
        deployment, err := d.app.GetDeployment(ctx, id)
2✔
1155
        if err != nil {
2✔
1156
                d.view.RenderInternalError(c, err)
×
1157
                return
×
1158
        }
×
1159

1160
        if deployment == nil {
2✔
1161
                d.view.RenderErrorNotFound(c)
×
1162
                return
×
1163
        }
×
1164

1165
        d.view.RenderSuccessGet(c, deployment)
2✔
1166
}
1167

1168
func (d *DeploymentsApiHandlers) GetDeploymentStats(c *gin.Context) {
1✔
1169
        ctx := c.Request.Context()
1✔
1170

1✔
1171
        id := c.Param("id")
1✔
1172

1✔
1173
        if !govalidator.IsUUID(id) {
1✔
1174
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1175
                return
×
1176
        }
×
1177

1178
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1179
        if err != nil {
1✔
1180
                d.view.RenderInternalError(c, err)
×
1181
                return
×
1182
        }
×
1183

1184
        if stats == nil {
1✔
1185
                d.view.RenderErrorNotFound(c)
×
1186
                return
×
1187
        }
×
1188

1189
        d.view.RenderSuccessGet(c, stats)
1✔
1190
}
1191

1192
func (d *DeploymentsApiHandlers) GetDeploymentsStats(c *gin.Context) {
1✔
1193

1✔
1194
        ctx := c.Request.Context()
1✔
1195

1✔
1196
        ids := model.DeploymentIDs{}
1✔
1197
        if err := c.ShouldBindJSON(&ids); err != nil {
1✔
1198
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1199
                return
×
1200
        }
×
1201

1202
        if len(ids.IDs) == 0 {
1✔
1203
                c.JSON(http.StatusOK, struct{}{})
×
1204
                return
×
1205
        }
×
1206

1207
        if err := ids.Validate(); err != nil {
2✔
1208
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1209
                return
1✔
1210
        }
1✔
1211

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

1222
        c.JSON(http.StatusOK, stats)
1✔
1223
}
1224

1225
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(c *gin.Context) {
×
1226
        ctx := c.Request.Context()
×
1227

×
1228
        id := c.Param("id")
×
1229

×
1230
        if !govalidator.IsUUID(id) {
×
1231
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1232
                return
×
1233
        }
×
1234

1235
        deployment, err := d.app.GetDeployment(ctx, id)
×
1236
        if err != nil {
×
1237
                d.view.RenderInternalError(c, err)
×
1238
                return
×
1239
        }
×
1240

1241
        if deployment == nil {
×
1242
                d.view.RenderErrorNotFound(c)
×
1243
                return
×
1244
        }
×
1245

1246
        d.view.RenderSuccessGet(c, deployment.DeviceList)
×
1247
}
1248

1249
func (d *DeploymentsApiHandlers) AbortDeployment(c *gin.Context) {
1✔
1250
        ctx := c.Request.Context()
1✔
1251

1✔
1252
        id := c.Param("id")
1✔
1253

1✔
1254
        if !govalidator.IsUUID(id) {
1✔
1255
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1256
                return
×
1257
        }
×
1258

1259
        // receive request body
1260
        var status struct {
1✔
1261
                Status model.DeviceDeploymentStatus
1✔
1262
        }
1✔
1263

1✔
1264
        err := c.ShouldBindJSON(&status)
1✔
1265
        if err != nil {
1✔
1266
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1267
                return
×
1268
        }
×
1269
        // "aborted" is the only supported status
1270
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1271
                d.view.RenderError(c, ErrUnexpectedDeploymentStatus, http.StatusBadRequest)
×
1272
        }
×
1273

1274
        l := log.FromContext(ctx)
1✔
1275
        l.Infof("Abort deployment: %s", id)
1✔
1276

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

1288
        // Abort deployments for devices and update deployment stats
1289
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1290
                d.view.RenderInternalError(c, err)
×
1291
        }
×
1292

1293
        d.view.RenderEmptySuccessResponse(c)
1✔
1294
}
1295

1296
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(c *gin.Context) {
3✔
1297
        var (
3✔
1298
                installed *model.InstalledDeviceDeployment
3✔
1299
                ctx       = c.Request.Context()
3✔
1300
                idata     = identity.FromContext(ctx)
3✔
1301
        )
3✔
1302
        if idata == nil {
4✔
1303
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
1✔
1304
                return
1✔
1305
        }
1✔
1306

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

1339
        if err := installed.Validate(); err != nil {
4✔
1340
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1341
                return
1✔
1342
        }
1✔
1343

1344
        request := &model.DeploymentNextRequest{
3✔
1345
                DeviceProvides: installed,
3✔
1346
        }
3✔
1347

3✔
1348
        d.getDeploymentForDevice(c, idata, request)
3✔
1349
}
1350

1351
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1352
        c *gin.Context,
1353
        idata *identity.Identity,
1354
        request *model.DeploymentNextRequest,
1355
) {
3✔
1356
        ctx := c.Request.Context()
3✔
1357

3✔
1358
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
3✔
1359
        if err != nil {
5✔
1360
                if err == app.ErrConflictingRequestData {
3✔
1361
                        d.view.RenderError(c, err, http.StatusConflict)
1✔
1362
                } else {
2✔
1363
                        d.view.RenderInternalError(c, err)
1✔
1364
                }
1✔
1365
                return
2✔
1366
        }
1367

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

1406
        d.view.RenderSuccessGet(c, deployment)
3✔
1407
}
1408

1409
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1410
        c *gin.Context,
1411
) {
2✔
1412
        ctx := c.Request.Context()
2✔
1413

2✔
1414
        did := c.Param("id")
2✔
1415

2✔
1416
        idata := identity.FromContext(ctx)
2✔
1417
        if idata == nil {
2✔
1418
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
1419
                return
×
1420
        }
×
1421

1422
        // receive request body
1423
        var report model.StatusReport
2✔
1424

2✔
1425
        err := c.ShouldBindJSON(&report)
2✔
1426
        if err != nil {
3✔
1427
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1428
                return
1✔
1429
        }
1✔
1430
        l := log.FromContext(ctx)
2✔
1431
        l.Infof("status: %+v", report)
2✔
1432
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
2✔
1433
                idata.Subject, model.DeviceDeploymentState{
2✔
1434
                        Status:   report.Status,
2✔
1435
                        SubState: report.SubState,
2✔
1436
                }); err != nil {
3✔
1437

1✔
1438
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
1✔
1439
                        d.view.RenderError(c, err, http.StatusConflict)
×
1440
                } else if err == app.ErrStorageNotFound {
2✔
1441
                        d.view.RenderErrorNotFound(c)
1✔
1442
                } else {
1✔
1443
                        d.view.RenderInternalError(c, err)
×
1444
                }
×
1445
                return
1✔
1446
        }
1447

1448
        d.view.RenderEmptySuccessResponse(c)
2✔
1449
}
1450

1451
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1452
        c *gin.Context,
1453
) {
2✔
1454
        ctx := c.Request.Context()
2✔
1455

2✔
1456
        did := c.Param("id")
2✔
1457

2✔
1458
        if !govalidator.IsUUID(did) {
2✔
1459
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1460
                return
×
1461
        }
×
1462

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

1475
        d.view.RenderSuccessGet(c, statuses)
2✔
1476
}
1477

1478
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1479
        c *gin.Context,
1480
) {
1✔
1481
        ctx := c.Request.Context()
1✔
1482

1✔
1483
        did := c.Param("id")
1✔
1484

1✔
1485
        if !govalidator.IsUUID(did) {
1✔
1486
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1487
                return
×
1488
        }
×
1489

1490
        page, perPage, err := rest.ParsePagingParameters(c.Request)
1✔
1491
        if err != nil {
1✔
1492
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1493
                return
×
1494
        }
×
1495

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

1509
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1510
        if err != nil {
1✔
1511
                switch err {
×
1512
                case app.ErrModelDeploymentNotFound:
×
1513
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1514
                        return
×
1515
                default:
×
1516
                        d.view.RenderInternalError(c, err)
×
1517
                        return
×
1518
                }
1519
        }
1520

1521
        hasNext := totalCount > int(page*perPage)
1✔
1522
        hints := rest.NewPagingHints().
1✔
1523
                SetPage(page).
1✔
1524
                SetPerPage(perPage).
1✔
1525
                SetHasNext(hasNext).
1✔
1526
                SetTotalCount(int64(totalCount))
1✔
1527

1✔
1528
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
1529
        if err != nil {
1✔
1530
                d.view.RenderInternalError(c, err)
×
1531
                return
×
1532
        }
×
1533

1534
        for _, l := range links {
2✔
1535
                c.Writer.Header().Add(hdrLink, l)
1✔
1536
        }
1✔
1537
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1538
        d.view.RenderSuccessGet(c, statuses)
1✔
1539
}
1540

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

3✔
1544
        createdBefore := vals.Get("created_before")
3✔
1545
        if createdBefore != "" {
5✔
1546
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
3✔
1547
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1548
                } else {
2✔
1549
                        query.CreatedBefore = &createdBeforeTime
1✔
1550
                }
1✔
1551
        }
1552

1553
        createdAfter := vals.Get("created_after")
3✔
1554
        if createdAfter != "" {
4✔
1555
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
1✔
1556
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1557
                } else {
1✔
1558
                        query.CreatedAfter = &createdAfterTime
1✔
1559
                }
1✔
1560
        }
1561

1562
        switch strings.ToLower(vals.Get("sort")) {
3✔
1563
        case model.SortDirectionAscending:
1✔
1564
                query.Sort = model.SortDirectionAscending
1✔
1565
        case "", model.SortDirectionDescending:
3✔
1566
                query.Sort = model.SortDirectionDescending
3✔
1567
        default:
×
1568
                return query, ErrInvalidSortDirection
×
1569
        }
1570

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

1586
        }
1587

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

1600
        return query, nil
×
1601
}
1602

1603
func ParseDeploymentLookupQueryV1(vals url.Values) (model.Query, error) {
3✔
1604
        query, err := ParseLookupQuery(vals)
3✔
1605
        if err != nil {
4✔
1606
                return query, err
1✔
1607
        }
1✔
1608

1609
        search := vals.Get("search")
3✔
1610
        if search != "" {
3✔
1611
                query.SearchText = search
×
1612
        }
×
1613

1614
        return query, nil
3✔
1615
}
1616

1617
func ParseDeploymentLookupQueryV2(vals url.Values) (model.Query, error) {
2✔
1618
        query, err := ParseLookupQuery(vals)
2✔
1619
        if err != nil {
2✔
1620
                return query, err
×
1621
        }
×
1622

1623
        query.Names = vals["name"]
2✔
1624
        query.IDs = vals["id"]
2✔
1625

2✔
1626
        return query, nil
2✔
1627
}
1628

1629
func parseEpochToTimestamp(epoch string) (time.Time, error) {
2✔
1630
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
3✔
1631
                return time.Time{}, errors.New("invalid timestamp: " + epoch)
1✔
1632
        } else {
2✔
1633
                return time.Unix(epochInt64, 0).UTC(), nil
1✔
1634
        }
1✔
1635
}
1636

1637
func (d *DeploymentsApiHandlers) LookupDeployment(c *gin.Context) {
3✔
1638
        ctx := c.Request.Context()
3✔
1639
        q := c.Request.URL.Query()
3✔
1640
        defer func() {
6✔
1641
                if search := q.Get("search"); search != "" {
3✔
1642
                        q.Set("search", Redacted)
×
1643
                        c.Request.URL.RawQuery = q.Encode()
×
1644
                }
×
1645
        }()
1646

1647
        query, err := ParseDeploymentLookupQueryV1(q)
3✔
1648
        if err != nil {
4✔
1649
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1650
                return
1✔
1651
        }
1✔
1652

1653
        page, perPage, err := rest.ParsePagingParameters(c.Request)
3✔
1654
        if err != nil {
4✔
1655
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1656
                return
1✔
1657
        }
1✔
1658
        query.Skip = int((page - 1) * perPage)
3✔
1659
        query.Limit = int(perPage + 1)
3✔
1660

3✔
1661
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
3✔
1662
        if err != nil {
4✔
1663
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1664
                return
1✔
1665
        }
1✔
1666
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
3✔
1667

3✔
1668
        len := len(deps)
3✔
1669
        hasNext := false
3✔
1670
        if int64(len) > perPage {
3✔
1671
                hasNext = true
×
1672
                len = int(perPage)
×
1673
        }
×
1674

1675
        hints := rest.NewPagingHints().
3✔
1676
                SetPage(page).
3✔
1677
                SetPerPage(perPage).
3✔
1678
                SetHasNext(hasNext).
3✔
1679
                SetTotalCount(int64(totalCount))
3✔
1680

3✔
1681
        links, err := rest.MakePagingHeaders(c.Request, hints)
3✔
1682
        if err != nil {
3✔
1683
                d.view.RenderInternalError(c, err)
×
1684
                return
×
1685
        }
×
1686
        for _, l := range links {
6✔
1687
                c.Writer.Header().Add(hdrLink, l)
3✔
1688
        }
3✔
1689

1690
        d.view.RenderSuccessGet(c, deps[:len])
3✔
1691
}
1692

1693
func (d *DeploymentsApiHandlers) LookupDeploymentV2(c *gin.Context) {
2✔
1694
        ctx := c.Request.Context()
2✔
1695
        q := c.Request.URL.Query()
2✔
1696
        defer func() {
4✔
1697
                if q.Has("name") {
3✔
1698
                        q["name"] = []string{Redacted}
1✔
1699
                        c.Request.URL.RawQuery = q.Encode()
1✔
1700
                }
1✔
1701
        }()
1702

1703
        query, err := ParseDeploymentLookupQueryV2(q)
2✔
1704
        if err != nil {
2✔
1705
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1706
                return
×
1707
        }
×
1708

1709
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1710
        if err != nil {
3✔
1711
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1712
                return
1✔
1713
        }
1✔
1714
        query.Skip = int((page - 1) * perPage)
2✔
1715
        query.Limit = int(perPage + 1)
2✔
1716

2✔
1717
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
2✔
1718
        if err != nil {
2✔
1719
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1720
                return
×
1721
        }
×
1722
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
2✔
1723

2✔
1724
        len := len(deps)
2✔
1725
        hasNext := false
2✔
1726
        if int64(len) > perPage {
3✔
1727
                hasNext = true
1✔
1728
                len = int(perPage)
1✔
1729
        }
1✔
1730

1731
        hints := rest.NewPagingHints().
2✔
1732
                SetPage(page).
2✔
1733
                SetPerPage(perPage).
2✔
1734
                SetHasNext(hasNext).
2✔
1735
                SetTotalCount(int64(totalCount))
2✔
1736

2✔
1737
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
1738
        if err != nil {
2✔
1739
                d.view.RenderInternalError(c, err)
×
1740
                return
×
1741
        }
×
1742
        for _, l := range links {
4✔
1743
                c.Writer.Header().Add(hdrLink, l)
2✔
1744
        }
2✔
1745

1746
        d.view.RenderSuccessGet(c, deps[:len])
2✔
1747
}
1748

1749
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(c *gin.Context) {
1✔
1750
        ctx := c.Request.Context()
1✔
1751

1✔
1752
        did := c.Param("id")
1✔
1753

1✔
1754
        idata := identity.FromContext(ctx)
1✔
1755
        if idata == nil {
1✔
1756
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
1757
                return
×
1758
        }
×
1759

1760
        // reuse DeploymentLog, device and deployment IDs are ignored when
1761
        // (un-)marshaling DeploymentLog to/from JSON
1762
        var log model.DeploymentLog
1✔
1763

1✔
1764
        err := c.ShouldBindJSON(&log)
1✔
1765
        if err != nil {
1✔
1766
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1767
                return
×
1768
        }
×
1769

1770
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1771
                did, log.Messages); err != nil {
1✔
1772

×
1773
                if err == app.ErrModelDeploymentNotFound {
×
1774
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1775
                } else {
×
1776
                        d.view.RenderInternalError(c, err)
×
1777
                }
×
1778
                return
×
1779
        }
1780

1781
        d.view.RenderEmptySuccessResponse(c)
1✔
1782
}
1783

1784
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(c *gin.Context) {
1✔
1785
        ctx := c.Request.Context()
1✔
1786

1✔
1787
        did := c.Param("id")
1✔
1788
        devid := c.Param("devid")
1✔
1789

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

1✔
1792
        if err != nil {
1✔
1793
                d.view.RenderInternalError(c, err)
×
1794
                return
×
1795
        }
×
1796

1797
        if depl == nil {
1✔
1798
                d.view.RenderErrorNotFound(c)
×
1799
                return
×
1800
        }
×
1801

1802
        d.view.RenderDeploymentLog(c, *depl)
1✔
1803
}
1804

1805
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(c *gin.Context) {
1✔
1806
        ctx := c.Request.Context()
1✔
1807

1✔
1808
        id := c.Param("id")
1✔
1809
        err := d.app.AbortDeviceDeployments(ctx, id)
1✔
1810

1✔
1811
        switch err {
1✔
1812
        case nil, app.ErrStorageNotFound:
1✔
1813
                d.view.RenderEmptySuccessResponse(c)
1✔
1814
        default:
1✔
1815
                d.view.RenderInternalError(c, err)
1✔
1816
        }
1817
}
1818

1819
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(c *gin.Context) {
1✔
1820
        ctx := c.Request.Context()
1✔
1821

1✔
1822
        id := c.Param("id")
1✔
1823
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
1✔
1824

1✔
1825
        switch err {
1✔
1826
        case nil, app.ErrStorageNotFound:
1✔
1827
                d.view.RenderEmptySuccessResponse(c)
1✔
1828
        default:
1✔
1829
                d.view.RenderInternalError(c, err)
1✔
1830
        }
1831
}
1832

1833
func (d *DeploymentsApiHandlers) ListDeviceDeployments(c *gin.Context) {
2✔
1834
        ctx := c.Request.Context()
2✔
1835
        d.listDeviceDeployments(ctx, c, true)
2✔
1836
}
2✔
1837

1838
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(c *gin.Context) {
2✔
1839
        ctx := c.Request.Context()
2✔
1840
        tenantID := c.Param("tenant")
2✔
1841
        if tenantID != "" {
4✔
1842
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
1843
                        Tenant:   tenantID,
2✔
1844
                        IsDevice: true,
2✔
1845
                })
2✔
1846
        }
2✔
1847
        d.listDeviceDeployments(ctx, c, true)
2✔
1848
}
1849

1850
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(c *gin.Context) {
2✔
1851
        ctx := c.Request.Context()
2✔
1852
        tenantID := c.Param("tenant")
2✔
1853
        if tenantID != "" {
4✔
1854
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
1855
                        Tenant:   tenantID,
2✔
1856
                        IsDevice: true,
2✔
1857
                })
2✔
1858
        }
2✔
1859
        d.listDeviceDeployments(ctx, c, false)
2✔
1860
}
1861

1862
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1863
        c *gin.Context, byDeviceID bool) {
2✔
1864

2✔
1865
        did := ""
2✔
1866
        var IDs []string
2✔
1867
        if byDeviceID {
4✔
1868
                did = c.Param("id")
2✔
1869
        } else {
4✔
1870
                values := c.Request.URL.Query()
2✔
1871
                if values.Has("id") && len(values["id"]) > 0 {
3✔
1872
                        IDs = values["id"]
1✔
1873
                } else {
3✔
1874
                        d.view.RenderError(c, ErrEmptyID, http.StatusBadRequest)
2✔
1875
                        return
2✔
1876
                }
2✔
1877
        }
1878

1879
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1880
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
3✔
1881
                err = rest.ErrQueryParmLimit(ParamPerPage)
1✔
1882
        }
1✔
1883
        if err != nil {
3✔
1884
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1885
                return
1✔
1886
        }
1✔
1887

1888
        lq := store.ListQueryDeviceDeployments{
2✔
1889
                Skip:     int((page - 1) * perPage),
2✔
1890
                Limit:    int(perPage),
2✔
1891
                DeviceID: did,
2✔
1892
                IDs:      IDs,
2✔
1893
        }
2✔
1894
        if status := c.Request.URL.Query().Get("status"); status != "" {
3✔
1895
                lq.Status = &status
1✔
1896
        }
1✔
1897
        if err = lq.Validate(); err != nil {
3✔
1898
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1899
                return
1✔
1900
        }
1✔
1901

1902
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
2✔
1903
        if err != nil {
3✔
1904
                d.view.RenderInternalError(c, err)
1✔
1905
                return
1✔
1906
        }
1✔
1907
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
2✔
1908

2✔
1909
        hasNext := totalCount > lq.Skip+len(deps)
2✔
1910

2✔
1911
        hints := rest.NewPagingHints().
2✔
1912
                SetPage(page).
2✔
1913
                SetPerPage(perPage).
2✔
1914
                SetHasNext(hasNext).
2✔
1915
                SetTotalCount(int64(totalCount))
2✔
1916

2✔
1917
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
1918
        if err != nil {
2✔
1919
                rest.RenderInternalError(c, err)
×
1920
                return
×
1921
        }
×
1922
        for _, l := range links {
4✔
1923
                c.Writer.Header().Add(hdrLink, l)
2✔
1924
        }
2✔
1925

1926
        d.view.RenderSuccessGet(c, deps)
2✔
1927
}
1928

1929
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(c *gin.Context) {
1✔
1930
        ctx := c.Request.Context()
1✔
1931
        tenantID := c.Param("tenantID")
1✔
1932
        if tenantID != "" {
1✔
1933
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
×
1934
                        Tenant:   tenantID,
×
1935
                        IsDevice: true,
×
1936
                })
×
1937
        }
×
1938

1939
        id := c.Param("id")
1✔
1940

1✔
1941
        // Decommission deployments for devices and update deployment stats
1✔
1942
        err := d.app.DecommissionDevice(ctx, id)
1✔
1943

1✔
1944
        switch err {
1✔
1945
        case nil, app.ErrStorageNotFound:
1✔
1946
                d.view.RenderEmptySuccessResponse(c)
1✔
1947
        default:
×
1948
                d.view.RenderInternalError(c, err)
×
1949

1950
        }
1951
}
1952

1953
// tenants
1954

1955
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(c *gin.Context) {
2✔
1956
        ctx := c.Request.Context()
2✔
1957

2✔
1958
        defer c.Request.Body.Close()
2✔
1959

2✔
1960
        tenant, err := model.ParseNewTenantReq(c.Request.Body)
2✔
1961
        if err != nil {
4✔
1962
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
1963
                return
2✔
1964
        }
2✔
1965

1966
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1967
        if err != nil {
1✔
1968
                d.view.RenderInternalError(c, err)
×
1969
                return
×
1970
        }
×
1971

1972
        c.Status(http.StatusCreated)
1✔
1973
}
1974

1975
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1976
        c *gin.Context,
1977
) {
2✔
1978
        tenantID := c.Param("tenant")
2✔
1979
        if tenantID == "" {
3✔
1980

1✔
1981
                d.view.RenderError(c, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1982
                return
1✔
1983
        }
1✔
1984
        c.Request = c.Request.WithContext(identity.WithContext(
2✔
1985
                c.Request.Context(),
2✔
1986
                &identity.Identity{Tenant: tenantID},
2✔
1987
        ))
2✔
1988
        d.LookupDeployment(c)
2✔
1989
}
1990

1991
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1992
        c *gin.Context,
1993
) {
3✔
1994

3✔
1995
        tenantID := c.Param("tenant")
3✔
1996

3✔
1997
        ctx := identity.WithContext(
3✔
1998
                c.Request.Context(),
3✔
1999
                &identity.Identity{Tenant: tenantID},
3✔
2000
        )
3✔
2001

3✔
2002
        settings, err := d.app.GetStorageSettings(ctx)
3✔
2003
        if err != nil {
4✔
2004
                d.view.RenderInternalError(c, err)
1✔
2005
                return
1✔
2006
        }
1✔
2007

2008
        d.view.RenderSuccessGet(c, settings)
3✔
2009
}
2010

2011
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
2012
        c *gin.Context,
2013
) {
3✔
2014

3✔
2015
        defer c.Request.Body.Close()
3✔
2016

3✔
2017
        tenantID := c.Param("tenant")
3✔
2018

3✔
2019
        ctx := identity.WithContext(
3✔
2020
                c.Request.Context(),
3✔
2021
                &identity.Identity{Tenant: tenantID},
3✔
2022
        )
3✔
2023

3✔
2024
        settings, err := model.ParseStorageSettingsRequest(c.Request.Body)
3✔
2025
        if err != nil {
6✔
2026
                d.view.RenderError(c, err, http.StatusBadRequest)
3✔
2027
                return
3✔
2028
        }
3✔
2029

2030
        err = d.app.SetStorageSettings(ctx, settings)
2✔
2031
        if err != nil {
3✔
2032
                d.view.RenderInternalError(c, err)
1✔
2033
                return
1✔
2034
        }
1✔
2035

2036
        c.Status(http.StatusNoContent)
2✔
2037
}
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

© 2026 Coveralls, Inc