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

mendersoftware / mender-server / 1932309727

17 Jul 2025 12:42PM UTC coverage: 65.498% (-0.02%) from 65.521%
1932309727

Pull #790

gitlab-ci

bahaa-ghazal
feat(deployments): Implement new v2 GET `/artifacts` endpoint

Ticket: MEN-8181
Changelog: Title
Signed-off-by: Bahaa Aldeen Ghazal <bahaa.ghazal@northern.tech>
Pull Request #790: feat(deployments): Implement new v2 GET `/artifacts` endpoint

142 of 237 new or added lines in 7 files covered. (59.92%)

2 existing lines in 1 file now uncovered.

32335 of 49368 relevant lines covered (65.5%)

1.39 hits per line

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

79.22
/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 {
4✔
230
                        continue
1✔
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
func getImageFilter(c *gin.Context, paginated bool) *model.ImageFilter {
1✔
325

1✔
326
        q := c.Request.URL.Query()
1✔
327

1✔
328
        names := c.QueryArray(ParamName)
1✔
329

1✔
330
        var exactNames []string
1✔
331
        var namePrefixes []string
1✔
332

1✔
333
        for _, name := range names {
2✔
334
                if strings.HasSuffix(name, `\*`) {
1✔
NEW
335
                        name = strings.TrimSuffix(name, `\*`) + "*"
×
NEW
336
                        exactNames = append(exactNames, name)
×
337
                } else if strings.HasSuffix(name, "*") {
1✔
NEW
338
                        namePrefixes = append(namePrefixes, strings.TrimSuffix(name, `*`))
×
339
                } else {
1✔
340
                        exactNames = append(exactNames, name)
1✔
341
                }
1✔
342
        }
343

344
        filter := &model.ImageFilter{
1✔
345
                ExactNames:   exactNames,
1✔
346
                NamePrefixes: namePrefixes,
1✔
347
                Description:  q.Get(ParamDescription),
1✔
348
                DeviceType:   q.Get(ParamDeviceType),
1✔
349
        }
1✔
350

1✔
351
        if paginated {
2✔
352
                // filter.Sort = q.Get(ParamSort)
1✔
353
                if page := q.Get(ParamPage); page != "" {
1✔
NEW
354
                        if i, err := strconv.Atoi(page); err == nil {
×
NEW
355
                                filter.Page = i
×
NEW
356
                        }
×
357
                }
358
                if perPage := q.Get(ParamPerPage); perPage != "" {
1✔
NEW
359
                        if i, err := strconv.Atoi(perPage); err == nil {
×
NEW
360
                                filter.PerPage = i
×
NEW
361
                        }
×
362
                }
363
                if filter.Page <= 0 {
2✔
364
                        filter.Page = 1
1✔
365
                }
1✔
366
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
2✔
367
                        filter.PerPage = DefaultPerPage
1✔
368
                }
1✔
369
        }
370

371
        return filter
1✔
372
}
373

374
type limitResponse struct {
375
        Limit uint64 `json:"limit"`
376
        Usage uint64 `json:"usage"`
377
}
378

379
func (d *DeploymentsApiHandlers) GetLimit(c *gin.Context) {
1✔
380

1✔
381
        name := c.Param("name")
1✔
382

1✔
383
        if !model.IsValidLimit(name) {
2✔
384
                d.view.RenderError(c,
1✔
385
                        errors.Errorf("unsupported limit %s", name),
1✔
386
                        http.StatusBadRequest)
1✔
387
                return
1✔
388
        }
1✔
389

390
        limit, err := d.app.GetLimit(c.Request.Context(), name)
1✔
391
        if err != nil {
2✔
392
                d.view.RenderInternalError(c, err)
1✔
393
                return
1✔
394
        }
1✔
395

396
        d.view.RenderSuccessGet(c, limitResponse{
1✔
397
                Limit: limit.Value,
1✔
398
                Usage: 0, // TODO fill this when ready
1✔
399
        })
1✔
400
}
401

402
// images
403

404
func (d *DeploymentsApiHandlers) GetImage(c *gin.Context) {
2✔
405

2✔
406
        id := c.Param("id")
2✔
407

2✔
408
        if !govalidator.IsUUID(id) {
3✔
409
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
410
                return
1✔
411
        }
1✔
412

413
        image, err := d.app.GetImage(c.Request.Context(), id)
2✔
414
        if err != nil {
2✔
415
                d.view.RenderInternalError(c, err)
×
416
                return
×
417
        }
×
418

419
        if image == nil {
3✔
420
                d.view.RenderErrorNotFound(c)
1✔
421
                return
1✔
422
        }
1✔
423

424
        d.view.RenderSuccessGet(c, image)
2✔
425
}
426

427
func (d *DeploymentsApiHandlers) GetImages(c *gin.Context) {
3✔
428

3✔
429
        defer redactReleaseName(c.Request)
3✔
430
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, false)
3✔
431

3✔
432
        list, _, err := d.app.ListImages(c.Request.Context(), filter)
3✔
433
        if err != nil {
4✔
434
                d.view.RenderInternalError(c, err)
1✔
435
                return
1✔
436
        }
1✔
437

438
        d.view.RenderSuccessGet(c, list)
3✔
439
}
440

441
func (d *DeploymentsApiHandlers) ListImages(c *gin.Context) {
1✔
442

1✔
443
        defer redactReleaseName(c.Request)
1✔
444
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, true)
1✔
445

1✔
446
        list, totalCount, err := d.app.ListImages(c.Request.Context(), filter)
1✔
447
        if err != nil {
2✔
448
                d.view.RenderInternalError(c, err)
1✔
449
                return
1✔
450
        }
1✔
451

452
        hasNext := totalCount > int(filter.Page*filter.PerPage)
1✔
453

1✔
454
        hints := rest.NewPagingHints().
1✔
455
                SetPage(int64(filter.Page)).
1✔
456
                SetPerPage(int64(filter.PerPage)).
1✔
457
                SetHasNext(hasNext).
1✔
458
                SetTotalCount(int64(totalCount))
1✔
459

1✔
460
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
461
        if err != nil {
1✔
462
                d.view.RenderInternalError(c, err)
×
463
                return
×
464
        }
×
465

466
        for _, l := range links {
2✔
467
                c.Writer.Header().Add(hdrLink, l)
1✔
468
        }
1✔
469
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
470

1✔
471
        d.view.RenderSuccessGet(c, list)
1✔
472
}
473

474
func (d *DeploymentsApiHandlers) ListImagesV2(c *gin.Context) {
1✔
475

1✔
476
        defer redactReleaseName(c.Request)
1✔
477
        filter := getImageFilter(c, true)
1✔
478

1✔
479
        if err := filter.Validate(); err != nil {
1✔
NEW
480
                d.view.RenderError(c, errors.Wrap(err, "invalid filter"), http.StatusBadRequest)
×
NEW
481
                return
×
NEW
482
        }
×
483

484
        // add one more item to the limit to check if there is a next page.
485
        filter.Limit = filter.PerPage + 1
1✔
486

1✔
487
        list, err := d.app.ListImagesV2(c.Request.Context(), filter)
1✔
488
        if err != nil {
2✔
489
                d.view.RenderInternalError(c, err)
1✔
490
                return
1✔
491
        }
1✔
492

493
        length := len(list)
1✔
494
        hasNext := false
1✔
495
        if length > filter.PerPage {
1✔
NEW
496
                hasNext = true
×
NEW
497
                length = filter.PerPage
×
NEW
498
        }
×
499

500
        hints := rest.NewPagingHints().
1✔
501
                SetPage(int64(filter.Page)).
1✔
502
                SetPerPage(int64(filter.PerPage)).
1✔
503
                SetHasNext(hasNext)
1✔
504

1✔
505
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
506
        if err != nil {
1✔
NEW
507
                d.view.RenderInternalError(c, err)
×
NEW
508
                return
×
NEW
509
        }
×
510

511
        for _, l := range links {
2✔
512
                c.Writer.Header().Add(hdrLink, l)
1✔
513
        }
1✔
514

515
        d.view.RenderSuccessGet(c, list[:length])
1✔
516
}
517

518
func (d *DeploymentsApiHandlers) DownloadLink(c *gin.Context) {
1✔
519

1✔
520
        id := c.Param("id")
1✔
521

1✔
522
        if !govalidator.IsUUID(id) {
1✔
523
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
524
                return
×
525
        }
×
526

527
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
528
        link, err := d.app.DownloadLink(c.Request.Context(), id,
1✔
529
                time.Duration(expireSeconds)*time.Second)
1✔
530
        if err != nil {
1✔
531
                d.view.RenderInternalError(c, err)
×
532
                return
×
533
        }
×
534

535
        if link == nil {
1✔
536
                d.view.RenderErrorNotFound(c)
×
537
                return
×
538
        }
×
539

540
        d.view.RenderSuccessGet(c, link)
1✔
541
}
542

543
func (d *DeploymentsApiHandlers) UploadLink(c *gin.Context) {
2✔
544

2✔
545
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
2✔
546
        link, err := d.app.UploadLink(
2✔
547
                c.Request.Context(),
2✔
548
                time.Duration(expireSeconds)*time.Second,
2✔
549
                d.config.EnableDirectUploadSkipVerify,
2✔
550
        )
2✔
551
        if err != nil {
3✔
552
                d.view.RenderInternalError(c, err)
1✔
553
                return
1✔
554
        }
1✔
555

556
        if link == nil {
3✔
557
                d.view.RenderErrorNotFound(c)
1✔
558
                return
1✔
559
        }
1✔
560

561
        d.view.RenderSuccessGet(c, link)
2✔
562
}
563

564
const maxMetadataSize = 2048
565

566
func (d *DeploymentsApiHandlers) CompleteUpload(c *gin.Context) {
2✔
567
        ctx := c.Request.Context()
2✔
568
        l := log.FromContext(ctx)
2✔
569

2✔
570
        artifactID := c.Param(ParamID)
2✔
571

2✔
572
        var metadata *model.DirectUploadMetadata
2✔
573
        if d.config.EnableDirectUploadSkipVerify {
3✔
574
                var directMetadata model.DirectUploadMetadata
1✔
575
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
576
                n, err := io.ReadFull(c.Request.Body, bodyBuffer)
1✔
577
                c.Request.Body.Close()
1✔
578
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
579
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
580
                } else {
1✔
581
                        err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
582
                        if err == nil {
2✔
583
                                if directMetadata.Validate() == nil {
2✔
584
                                        metadata = &directMetadata
1✔
585
                                }
1✔
586
                        } else {
1✔
587
                                l.Errorf("error parsing json data: %s", err.Error())
1✔
588
                        }
1✔
589
                }
590
        }
591

592
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
2✔
593
        switch errors.Cause(err) {
2✔
594
        case nil:
2✔
595
                // c.Writer.Header().Set("Link", "FEAT: Upload status API")
2✔
596
                c.Status(http.StatusAccepted)
2✔
597
        case app.ErrUploadNotFound:
1✔
598
                d.view.RenderErrorNotFound(c)
1✔
599
        default:
1✔
600
                d.view.RenderInternalError(c, err)
1✔
601
        }
602
}
603

604
func (d *DeploymentsApiHandlers) DownloadConfiguration(c *gin.Context) {
3✔
605
        if d.config.PresignSecret == nil {
4✔
606
                d.view.RenderErrorNotFound(c)
1✔
607
                return
1✔
608
        }
1✔
609
        var (
3✔
610
                deviceID, _     = url.PathUnescape(c.Param(ParamDeviceID))
3✔
611
                deviceType, _   = url.PathUnescape(c.Param(ParamDeviceType))
3✔
612
                deploymentID, _ = url.PathUnescape(c.Param(ParamDeploymentID))
3✔
613
        )
3✔
614
        if deviceID == "" || deviceType == "" || deploymentID == "" {
3✔
615
                d.view.RenderErrorNotFound(c)
×
616
                return
×
617
        }
×
618

619
        var (
3✔
620
                tenantID string
3✔
621
                q        = c.Request.URL.Query()
3✔
622
                err      error
3✔
623
        )
3✔
624
        tenantID = q.Get(ParamTenantID)
3✔
625
        sig := model.NewRequestSignature(c.Request, d.config.PresignSecret)
3✔
626
        if err = sig.Validate(); err != nil {
6✔
627
                switch cause := errors.Cause(err); cause {
3✔
628
                case model.ErrLinkExpired:
1✔
629
                        d.view.RenderError(c, cause, http.StatusForbidden)
1✔
630
                default:
3✔
631
                        d.view.RenderError(c,
3✔
632
                                errors.Wrap(err, "invalid request parameters"),
3✔
633
                                http.StatusBadRequest,
3✔
634
                        )
3✔
635
                }
636
                return
3✔
637
        }
638

639
        if !sig.VerifyHMAC256() {
4✔
640
                d.view.RenderError(c,
2✔
641
                        errors.New("signature invalid"),
2✔
642
                        http.StatusForbidden,
2✔
643
                )
2✔
644
                return
2✔
645
        }
2✔
646

647
        // Validate request signature
648
        ctx := identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
649
                Subject:  deviceID,
2✔
650
                Tenant:   tenantID,
2✔
651
                IsDevice: true,
2✔
652
        })
2✔
653

2✔
654
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
2✔
655
        if err != nil {
3✔
656
                switch cause := errors.Cause(err); cause {
1✔
657
                case app.ErrModelDeploymentNotFound:
1✔
658
                        d.view.RenderError(c,
1✔
659
                                errors.Errorf(
1✔
660
                                        "deployment with id '%s' not found",
1✔
661
                                        deploymentID,
1✔
662
                                ),
1✔
663
                                http.StatusNotFound,
1✔
664
                        )
1✔
665
                default:
1✔
666
                        d.view.RenderInternalError(c, err)
1✔
667
                }
668
                return
1✔
669
        }
670
        artifactPayload, err := io.ReadAll(artifact)
2✔
671
        if err != nil {
3✔
672
                d.view.RenderInternalError(c, err)
1✔
673
                return
1✔
674
        }
1✔
675

676
        rw := c.Writer
2✔
677
        hdr := rw.Header()
2✔
678
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
2✔
679
        hdr.Set("Content-Type", app.ArtifactContentType)
2✔
680
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
2✔
681
        c.Status(http.StatusOK)
2✔
682
        _, err = rw.Write(artifactPayload)
2✔
683
        if err != nil {
2✔
684
                // There's not anything we can do here in terms of the response.
×
685
                _ = c.Error(err)
×
686
        }
×
687
}
688

689
func (d *DeploymentsApiHandlers) DeleteImage(c *gin.Context) {
1✔
690

1✔
691
        id := c.Param("id")
1✔
692

1✔
693
        if !govalidator.IsUUID(id) {
1✔
694
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
695
                return
×
696
        }
×
697

698
        if err := d.app.DeleteImage(c.Request.Context(), id); err != nil {
2✔
699
                switch err {
1✔
700
                default:
×
701
                        d.view.RenderInternalError(c, err)
×
702
                case app.ErrImageMetaNotFound:
×
703
                        d.view.RenderErrorNotFound(c)
×
704
                case app.ErrModelImageInActiveDeployment:
1✔
705
                        d.view.RenderError(c, ErrArtifactUsedInActiveDeployment, http.StatusConflict)
1✔
706
                }
707
                return
1✔
708
        }
709

710
        d.view.RenderSuccessDelete(c)
1✔
711
}
712

713
func (d *DeploymentsApiHandlers) EditImage(c *gin.Context) {
×
714

×
715
        id := c.Param("id")
×
716

×
717
        if !govalidator.IsUUID(id) {
×
718
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
719
                return
×
720
        }
×
721

722
        constructor, err := getImageMetaFromBody(c)
×
723
        if err != nil {
×
724
                d.view.RenderError(
×
725
                        c,
×
726
                        errors.Wrap(err, "Validating request body"),
×
727
                        http.StatusBadRequest,
×
728
                )
×
729
                return
×
730
        }
×
731

732
        found, err := d.app.EditImage(c.Request.Context(), id, constructor)
×
733
        if err != nil {
×
734
                if err == app.ErrModelImageUsedInAnyDeployment {
×
735
                        d.view.RenderError(c, err, http.StatusUnprocessableEntity)
×
736
                        return
×
737
                }
×
738
                d.view.RenderInternalError(c, err)
×
739
                return
×
740
        }
741

742
        if !found {
×
743
                d.view.RenderErrorNotFound(c)
×
744
                return
×
745
        }
×
746

747
        d.view.RenderSuccessPut(c)
×
748
}
749

750
func getImageMetaFromBody(c *gin.Context) (*model.ImageMeta, error) {
×
751

×
752
        var constructor *model.ImageMeta
×
753

×
754
        if err := c.ShouldBindJSON(&constructor); err != nil {
×
755
                return nil, err
×
756
        }
×
757

758
        if err := constructor.Validate(); err != nil {
×
759
                return nil, err
×
760
        }
×
761

762
        return constructor, nil
×
763
}
764

765
// NewImage is the Multipart Image/Meta upload handler.
766
// Request should be of type "multipart/form-data". The parts are
767
// key/value pairs of metadata information except the last one,
768
// which must contain the artifact file.
769
func (d *DeploymentsApiHandlers) NewImage(c *gin.Context) {
3✔
770
        d.newImageWithContext(c.Request.Context(), c)
3✔
771
}
3✔
772

773
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(c *gin.Context) {
3✔
774

3✔
775
        tenantID := c.Param("tenant")
3✔
776

3✔
777
        if tenantID == "" {
3✔
778
                rest.RenderError(
×
779
                        c,
×
780
                        http.StatusBadRequest,
×
781
                        fmt.Errorf("missing tenant id in path"),
×
782
                )
×
783
                return
×
784
        }
×
785

786
        var ctx context.Context
3✔
787
        if tenantID != "default" {
5✔
788
                ident := &identity.Identity{Tenant: tenantID}
2✔
789
                ctx = identity.WithContext(c.Request.Context(), ident)
2✔
790
        } else {
4✔
791
                ctx = c.Request.Context()
2✔
792
        }
2✔
793

794
        d.newImageWithContext(ctx, c)
3✔
795
}
796

797
func (d *DeploymentsApiHandlers) newImageWithContext(
798
        ctx context.Context,
799
        c *gin.Context,
800
) {
3✔
801

3✔
802
        formReader, err := c.Request.MultipartReader()
3✔
803
        if err != nil {
5✔
804
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
805
                return
2✔
806
        }
2✔
807

808
        // parse multipart message
809
        multipartUploadMsg, err := d.ParseMultipart(formReader)
3✔
810

3✔
811
        if err != nil {
5✔
812
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
813
                return
2✔
814
        }
2✔
815

816
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
3✔
817
        if err == nil {
6✔
818
                d.view.RenderSuccessPost(c, imgID)
3✔
819
                return
3✔
820
        }
3✔
821
        var cErr *model.ConflictError
2✔
822
        if errors.As(err, &cErr) {
3✔
823
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
1✔
824
                c.JSON(http.StatusConflict, cErr)
1✔
825
                return
1✔
826
        }
1✔
827
        cause := errors.Cause(err)
1✔
828
        switch cause {
1✔
829
        default:
×
830
                d.view.RenderInternalError(c, err)
×
831
                return
×
832
        case app.ErrModelArtifactNotUnique:
×
833
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
×
834
                return
×
835
        case app.ErrModelParsingArtifactFailed:
1✔
836
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
837
                return
1✔
838
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
839
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
×
840
                return
×
841
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
842
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
843
                io.ErrUnexpectedEOF:
×
844
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
845
                return
×
846
        }
847
}
848

849
func formatArtifactUploadError(err error) error {
2✔
850
        // remove generic message
2✔
851
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
852

2✔
853
        // handle specific cases
2✔
854

2✔
855
        if strings.Contains(errMsg, "invalid checksum") {
2✔
856
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
857
        }
×
858

859
        if strings.Contains(errMsg, "unsupported version") {
2✔
860
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
861
                        "; supported versions are: 1, 2")
×
862
        }
×
863

864
        return errors.New(errMsg)
2✔
865
}
866

867
// GenerateImage s the multipart Raw Data/Meta upload handler.
868
// Request should be of type "multipart/form-data". The parts are
869
// key/valyue pairs of metadata information except the last one,
870
// which must contain the file containing the raw data to be processed
871
// into an artifact.
872
func (d *DeploymentsApiHandlers) GenerateImage(c *gin.Context) {
3✔
873

3✔
874
        formReader, err := c.Request.MultipartReader()
3✔
875
        if err != nil {
4✔
876
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
877
                return
1✔
878
        }
1✔
879

880
        // parse multipart message
881
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
3✔
882
        if err != nil {
4✔
883
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
884
                return
1✔
885
        }
1✔
886

887
        tokenFields := strings.Fields(c.Request.Header.Get("Authorization"))
3✔
888
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
6✔
889
                multipartMsg.Token = tokenFields[1]
3✔
890
        }
3✔
891

892
        imgID, err := d.app.GenerateImage(c.Request.Context(), multipartMsg)
3✔
893
        cause := errors.Cause(err)
3✔
894
        switch cause {
3✔
895
        default:
1✔
896
                d.view.RenderInternalError(c, err)
1✔
897
        case nil:
3✔
898
                d.view.RenderSuccessPost(c, imgID)
3✔
899
        case app.ErrModelArtifactNotUnique:
1✔
900
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
1✔
901
        case app.ErrModelParsingArtifactFailed:
1✔
902
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
903
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
904
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
1✔
905
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
906
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
907
                io.ErrUnexpectedEOF:
×
908
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
909
        }
910
}
911

912
// ParseMultipart parses multipart/form-data message.
913
func (d *DeploymentsApiHandlers) ParseMultipart(
914
        r *multipart.Reader,
915
) (*model.MultipartUploadMsg, error) {
3✔
916
        uploadMsg := &model.MultipartUploadMsg{
3✔
917
                MetaConstructor: &model.ImageMeta{},
3✔
918
        }
3✔
919
        var size int64
3✔
920
        // Parse the multipart form sequentially. To remain backward compatible
3✔
921
        // all form names that are not part of the API are ignored.
3✔
922
        for {
6✔
923
                part, err := r.NextPart()
3✔
924
                if err != nil {
5✔
925
                        if err == io.EOF {
4✔
926
                                // The whole message has been consumed without
2✔
927
                                // the "artifact" form part.
2✔
928
                                return nil, ErrArtifactFileMissing
2✔
929
                        }
2✔
930
                        return nil, err
×
931
                }
932
                switch strings.ToLower(part.FormName()) {
3✔
933
                case "description":
3✔
934
                        // Add description to the metadata
3✔
935
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
936
                        dscr, err := io.ReadAll(reader)
3✔
937
                        if err != nil {
3✔
938
                                return nil, errors.Wrap(err,
×
939
                                        "failed to read form value 'description'",
×
940
                                )
×
941
                        }
×
942
                        uploadMsg.MetaConstructor.Description = string(dscr)
3✔
943

944
                case "size":
3✔
945
                        // Add size limit to the metadata
3✔
946
                        reader := utils.ReadAtMost(part, 20)
3✔
947
                        sz, err := io.ReadAll(reader)
3✔
948
                        if err != nil {
4✔
949
                                return nil, errors.Wrap(err,
1✔
950
                                        "failed to read form value 'size'",
1✔
951
                                )
1✔
952
                        }
1✔
953
                        size, err = strconv.ParseInt(string(sz), 10, 64)
3✔
954
                        if err != nil {
3✔
955
                                return nil, err
×
956
                        }
×
957
                        if size > d.config.MaxImageSize {
3✔
958
                                return nil, ErrModelArtifactFileTooLarge
×
959
                        }
×
960

961
                case "artifact_id":
3✔
962
                        // Add artifact id to the metadata (must be a valid UUID).
3✔
963
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
964
                        b, err := io.ReadAll(reader)
3✔
965
                        if err != nil {
3✔
966
                                return nil, errors.Wrap(err,
×
967
                                        "failed to read form value 'artifact_id'",
×
968
                                )
×
969
                        }
×
970
                        id := string(b)
3✔
971
                        if !govalidator.IsUUID(id) {
5✔
972
                                return nil, errors.New(
2✔
973
                                        "artifact_id is not a valid UUID",
2✔
974
                                )
2✔
975
                        }
2✔
976
                        uploadMsg.ArtifactID = id
2✔
977

978
                case "artifact":
3✔
979
                        // Assign the form-data payload to the artifact reader
3✔
980
                        // and return. The content is consumed elsewhere.
3✔
981
                        if size > 0 {
6✔
982
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
3✔
983
                        } else {
4✔
984
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
1✔
985
                                        part,
1✔
986
                                        d.config.MaxImageSize,
1✔
987
                                )
1✔
988
                        }
1✔
989
                        return uploadMsg, nil
3✔
990

991
                default:
2✔
992
                        // Ignore all non-API sections.
2✔
993
                        continue
2✔
994
                }
995
        }
996
}
997

998
// ParseGenerateImageMultipart parses multipart/form-data message.
999
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
1000
        r *multipart.Reader,
1001
) (*model.MultipartGenerateImageMsg, error) {
3✔
1002
        msg := &model.MultipartGenerateImageMsg{}
3✔
1003
        var size int64
3✔
1004

3✔
1005
ParseLoop:
3✔
1006
        for {
6✔
1007
                part, err := r.NextPart()
3✔
1008
                if err != nil {
4✔
1009
                        if err == io.EOF {
2✔
1010
                                break
1✔
1011
                        }
1012
                        return nil, err
×
1013
                }
1014
                switch strings.ToLower(part.FormName()) {
3✔
1015
                case "args":
3✔
1016
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1017
                        b, err := io.ReadAll(reader)
3✔
1018
                        if err != nil {
3✔
1019
                                return nil, errors.Wrap(err,
×
1020
                                        "failed to read form value 'args'",
×
1021
                                )
×
1022
                        }
×
1023
                        msg.Args = string(b)
3✔
1024

1025
                case "description":
3✔
1026
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1027
                        b, err := io.ReadAll(reader)
3✔
1028
                        if err != nil {
3✔
1029
                                return nil, errors.Wrap(err,
×
1030
                                        "failed to read form value 'description'",
×
1031
                                )
×
1032
                        }
×
1033
                        msg.Description = string(b)
3✔
1034

1035
                case "device_types_compatible":
3✔
1036
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1037
                        b, err := io.ReadAll(reader)
3✔
1038
                        if err != nil {
3✔
1039
                                return nil, errors.Wrap(err,
×
1040
                                        "failed to read form value 'device_types_compatible'",
×
1041
                                )
×
1042
                        }
×
1043
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
3✔
1044

1045
                case "file":
3✔
1046
                        if size > 0 {
4✔
1047
                                msg.FileReader = utils.ReadExactly(part, size)
1✔
1048
                        } else {
4✔
1049
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxGenerateDataSize)
3✔
1050
                        }
3✔
1051
                        break ParseLoop
3✔
1052

1053
                case "name":
3✔
1054
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1055
                        b, err := io.ReadAll(reader)
3✔
1056
                        if err != nil {
3✔
1057
                                return nil, errors.Wrap(err,
×
1058
                                        "failed to read form value 'name'",
×
1059
                                )
×
1060
                        }
×
1061
                        msg.Name = string(b)
3✔
1062

1063
                case "type":
3✔
1064
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1065
                        b, err := io.ReadAll(reader)
3✔
1066
                        if err != nil {
3✔
1067
                                return nil, errors.Wrap(err,
×
1068
                                        "failed to read form value 'type'",
×
1069
                                )
×
1070
                        }
×
1071
                        msg.Type = string(b)
3✔
1072

1073
                case "size":
1✔
1074
                        // Add size limit to the metadata
1✔
1075
                        reader := utils.ReadAtMost(part, 20)
1✔
1076
                        sz, err := io.ReadAll(reader)
1✔
1077
                        if err != nil {
2✔
1078
                                return nil, errors.Wrap(err,
1✔
1079
                                        "failed to read form value 'size'",
1✔
1080
                                )
1✔
1081
                        }
1✔
1082
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
1083
                        if err != nil {
1✔
1084
                                return nil, err
×
1085
                        }
×
1086
                        if size > d.config.MaxGenerateDataSize {
1✔
1087
                                return nil, ErrModelArtifactFileTooLarge
×
1088
                        }
×
1089

1090
                default:
×
1091
                        // Ignore non-API sections.
×
1092
                        continue
×
1093
                }
1094
        }
1095

1096
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
3✔
1097
}
1098

1099
// deployments
1100
func (d *DeploymentsApiHandlers) createDeployment(
1101
        c *gin.Context,
1102
        ctx context.Context,
1103
        group string,
1104
) {
3✔
1105
        constructor, err := d.getDeploymentConstructorFromBody(c, group)
3✔
1106
        if err != nil {
6✔
1107
                d.view.RenderError(
3✔
1108
                        c,
3✔
1109
                        errors.Wrap(err, "Validating request body"),
3✔
1110
                        http.StatusBadRequest,
3✔
1111
                )
3✔
1112
                return
3✔
1113
        }
3✔
1114

1115
        id, err := d.app.CreateDeployment(ctx, constructor)
3✔
1116
        switch err {
3✔
1117
        case nil:
3✔
1118
                location := fmt.Sprintf("%s/%s", ApiUrlManagement+ApiUrlManagementDeployments, id)
3✔
1119
                c.Writer.Header().Add("Location", location)
3✔
1120
                c.Status(http.StatusCreated)
3✔
1121
        case app.ErrNoArtifact:
1✔
1122
                d.view.RenderError(c, err, http.StatusUnprocessableEntity)
1✔
1123
        case app.ErrNoDevices:
1✔
1124
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1125
        case app.ErrConflictingDeployment:
2✔
1126
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1127
        default:
1✔
1128
                d.view.RenderInternalError(c, err)
1✔
1129
        }
1130
}
1131

1132
func (d *DeploymentsApiHandlers) PostDeployment(c *gin.Context) {
3✔
1133
        ctx := c.Request.Context()
3✔
1134

3✔
1135
        d.createDeployment(c, ctx, "")
3✔
1136
}
3✔
1137

1138
func (d *DeploymentsApiHandlers) DeployToGroup(c *gin.Context) {
2✔
1139
        ctx := c.Request.Context()
2✔
1140

2✔
1141
        group := c.Param("name")
2✔
1142
        if len(group) < 1 {
2✔
1143
                d.view.RenderError(c, ErrMissingGroupName, http.StatusBadRequest)
×
1144
        }
×
1145
        d.createDeployment(c, ctx, group)
2✔
1146
}
1147

1148
// parseDeviceConfigurationDeploymentPathParams parses expected params
1149
// and check if the params are not empty
1150
func parseDeviceConfigurationDeploymentPathParams(c *gin.Context) (string, string, string, error) {
3✔
1151
        tenantID := c.Param("tenant")
3✔
1152
        deviceID := c.Param(ParamDeviceID)
3✔
1153
        if deviceID == "" {
3✔
1154
                return "", "", "", errors.New("device ID missing")
×
1155
        }
×
1156
        deploymentID := c.Param(ParamDeploymentID)
3✔
1157
        if deploymentID == "" {
3✔
1158
                return "", "", "", errors.New("deployment ID missing")
×
1159
        }
×
1160
        return tenantID, deviceID, deploymentID, nil
3✔
1161
}
1162

1163
// getConfigurationDeploymentConstructorFromBody extracts configuration
1164
// deployment constructor from the request body and validates it
1165
func getConfigurationDeploymentConstructorFromBody(c *gin.Context) (
1166
        *model.ConfigurationDeploymentConstructor, error) {
3✔
1167

3✔
1168
        var constructor *model.ConfigurationDeploymentConstructor
3✔
1169

3✔
1170
        if err := c.ShouldBindJSON(&constructor); err != nil {
5✔
1171
                return nil, err
2✔
1172
        }
2✔
1173

1174
        if err := constructor.Validate(); err != nil {
4✔
1175
                return nil, err
2✔
1176
        }
2✔
1177

1178
        return constructor, nil
2✔
1179
}
1180

1181
// device configuration deployment handler
1182
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1183
        c *gin.Context,
1184
) {
3✔
1185

3✔
1186
        // get path params
3✔
1187
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(c)
3✔
1188
        if err != nil {
3✔
1189
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1190
                return
×
1191
        }
×
1192

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

3✔
1196
        constructor, err := getConfigurationDeploymentConstructorFromBody(c)
3✔
1197
        if err != nil {
6✔
1198
                d.view.RenderError(
3✔
1199
                        c,
3✔
1200
                        errors.Wrap(err, "Validating request body"),
3✔
1201
                        http.StatusBadRequest,
3✔
1202
                )
3✔
1203
                return
3✔
1204
        }
3✔
1205

1206
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
2✔
1207
        switch err {
2✔
1208
        default:
1✔
1209
                d.view.RenderInternalError(c, err)
1✔
1210
        case nil:
2✔
1211
                c.Request.URL.Path = "./deployments"
2✔
1212
                d.view.RenderSuccessPost(c, id)
2✔
1213
        case app.ErrDuplicateDeployment:
2✔
1214
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1215
        case app.ErrInvalidDeploymentID:
1✔
1216
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1217
        }
1218
}
1219

1220
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1221
        c *gin.Context,
1222
        group string,
1223
) (*model.DeploymentConstructor, error) {
3✔
1224
        var constructor *model.DeploymentConstructor
3✔
1225
        if err := c.ShouldBindJSON(&constructor); err != nil {
4✔
1226
                return nil, err
1✔
1227
        }
1✔
1228

1229
        constructor.Group = group
3✔
1230

3✔
1231
        if err := constructor.ValidateNew(); err != nil {
6✔
1232
                return nil, err
3✔
1233
        }
3✔
1234

1235
        return constructor, nil
3✔
1236
}
1237

1238
func (d *DeploymentsApiHandlers) GetDeployment(c *gin.Context) {
2✔
1239
        ctx := c.Request.Context()
2✔
1240

2✔
1241
        id := c.Param("id")
2✔
1242

2✔
1243
        if !govalidator.IsUUID(id) {
3✔
1244
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
1245
                return
1✔
1246
        }
1✔
1247

1248
        deployment, err := d.app.GetDeployment(ctx, id)
2✔
1249
        if err != nil {
2✔
1250
                d.view.RenderInternalError(c, err)
×
1251
                return
×
1252
        }
×
1253

1254
        if deployment == nil {
2✔
1255
                d.view.RenderErrorNotFound(c)
×
1256
                return
×
1257
        }
×
1258

1259
        d.view.RenderSuccessGet(c, deployment)
2✔
1260
}
1261

1262
func (d *DeploymentsApiHandlers) GetDeploymentStats(c *gin.Context) {
1✔
1263
        ctx := c.Request.Context()
1✔
1264

1✔
1265
        id := c.Param("id")
1✔
1266

1✔
1267
        if !govalidator.IsUUID(id) {
1✔
1268
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1269
                return
×
1270
        }
×
1271

1272
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1273
        if err != nil {
1✔
1274
                d.view.RenderInternalError(c, err)
×
1275
                return
×
1276
        }
×
1277

1278
        if stats == nil {
1✔
1279
                d.view.RenderErrorNotFound(c)
×
1280
                return
×
1281
        }
×
1282

1283
        d.view.RenderSuccessGet(c, stats)
1✔
1284
}
1285

1286
func (d *DeploymentsApiHandlers) GetDeploymentsStats(c *gin.Context) {
1✔
1287

1✔
1288
        ctx := c.Request.Context()
1✔
1289

1✔
1290
        ids := model.DeploymentIDs{}
1✔
1291
        if err := c.ShouldBindJSON(&ids); err != nil {
1✔
1292
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1293
                return
×
1294
        }
×
1295

1296
        if len(ids.IDs) == 0 {
1✔
1297
                c.JSON(http.StatusOK, struct{}{})
×
1298
                return
×
1299
        }
×
1300

1301
        if err := ids.Validate(); err != nil {
2✔
1302
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1303
                return
1✔
1304
        }
1✔
1305

1306
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
1✔
1307
        if err != nil {
2✔
1308
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
2✔
1309
                        d.view.RenderError(c, err, http.StatusNotFound)
1✔
1310
                        return
1✔
1311
                }
1✔
1312
                d.view.RenderInternalError(c, err)
1✔
1313
                return
1✔
1314
        }
1315

1316
        c.JSON(http.StatusOK, stats)
1✔
1317
}
1318

1319
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(c *gin.Context) {
×
1320
        ctx := c.Request.Context()
×
1321

×
1322
        id := c.Param("id")
×
1323

×
1324
        if !govalidator.IsUUID(id) {
×
1325
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1326
                return
×
1327
        }
×
1328

1329
        deployment, err := d.app.GetDeployment(ctx, id)
×
1330
        if err != nil {
×
1331
                d.view.RenderInternalError(c, err)
×
1332
                return
×
1333
        }
×
1334

1335
        if deployment == nil {
×
1336
                d.view.RenderErrorNotFound(c)
×
1337
                return
×
1338
        }
×
1339

1340
        d.view.RenderSuccessGet(c, deployment.DeviceList)
×
1341
}
1342

1343
func (d *DeploymentsApiHandlers) AbortDeployment(c *gin.Context) {
1✔
1344
        ctx := c.Request.Context()
1✔
1345

1✔
1346
        id := c.Param("id")
1✔
1347

1✔
1348
        if !govalidator.IsUUID(id) {
1✔
1349
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1350
                return
×
1351
        }
×
1352

1353
        // receive request body
1354
        var status struct {
1✔
1355
                Status model.DeviceDeploymentStatus
1✔
1356
        }
1✔
1357

1✔
1358
        err := c.ShouldBindJSON(&status)
1✔
1359
        if err != nil {
1✔
1360
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1361
                return
×
1362
        }
×
1363
        // "aborted" is the only supported status
1364
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1365
                d.view.RenderError(c, ErrUnexpectedDeploymentStatus, http.StatusBadRequest)
×
1366
        }
×
1367

1368
        l := log.FromContext(ctx)
1✔
1369
        l.Infof("Abort deployment: %s", id)
1✔
1370

1✔
1371
        // Check if deployment is finished
1✔
1372
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1373
        if err != nil {
1✔
1374
                d.view.RenderInternalError(c, err)
×
1375
                return
×
1376
        }
×
1377
        if isDeploymentFinished {
2✔
1378
                d.view.RenderError(c, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity)
1✔
1379
                return
1✔
1380
        }
1✔
1381

1382
        // Abort deployments for devices and update deployment stats
1383
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1384
                d.view.RenderInternalError(c, err)
×
1385
        }
×
1386

1387
        d.view.RenderEmptySuccessResponse(c)
1✔
1388
}
1389

1390
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(c *gin.Context) {
3✔
1391
        var (
3✔
1392
                installed *model.InstalledDeviceDeployment
3✔
1393
                ctx       = c.Request.Context()
3✔
1394
                idata     = identity.FromContext(ctx)
3✔
1395
        )
3✔
1396
        if idata == nil {
4✔
1397
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
1✔
1398
                return
1✔
1399
        }
1✔
1400

1401
        q := c.Request.URL.Query()
3✔
1402
        defer func() {
6✔
1403
                var reEncode bool = false
3✔
1404
                if name := q.Get(ParamArtifactName); name != "" {
6✔
1405
                        q.Set(ParamArtifactName, Redacted)
3✔
1406
                        reEncode = true
3✔
1407
                }
3✔
1408
                if typ := q.Get(ParamDeviceType); typ != "" {
6✔
1409
                        q.Set(ParamDeviceType, Redacted)
3✔
1410
                        reEncode = true
3✔
1411
                }
3✔
1412
                if reEncode {
6✔
1413
                        c.Request.URL.RawQuery = q.Encode()
3✔
1414
                }
3✔
1415
        }()
1416
        if strings.EqualFold(c.Request.Method, http.MethodPost) {
5✔
1417
                // POST
2✔
1418
                installed = new(model.InstalledDeviceDeployment)
2✔
1419
                if err := c.ShouldBindJSON(&installed); err != nil {
3✔
1420
                        d.view.RenderError(c,
1✔
1421
                                errors.Wrap(err, "invalid schema"),
1✔
1422
                                http.StatusBadRequest)
1✔
1423
                        return
1✔
1424
                }
1✔
1425
        } else {
3✔
1426
                // GET or HEAD
3✔
1427
                installed = &model.InstalledDeviceDeployment{
3✔
1428
                        ArtifactName: q.Get(ParamArtifactName),
3✔
1429
                        DeviceType:   q.Get(ParamDeviceType),
3✔
1430
                }
3✔
1431
        }
3✔
1432

1433
        if err := installed.Validate(); err != nil {
4✔
1434
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1435
                return
1✔
1436
        }
1✔
1437

1438
        request := &model.DeploymentNextRequest{
3✔
1439
                DeviceProvides: installed,
3✔
1440
        }
3✔
1441

3✔
1442
        d.getDeploymentForDevice(c, idata, request)
3✔
1443
}
1444

1445
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1446
        c *gin.Context,
1447
        idata *identity.Identity,
1448
        request *model.DeploymentNextRequest,
1449
) {
3✔
1450
        ctx := c.Request.Context()
3✔
1451

3✔
1452
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
3✔
1453
        if err != nil {
5✔
1454
                if err == app.ErrConflictingRequestData {
3✔
1455
                        d.view.RenderError(c, err, http.StatusConflict)
1✔
1456
                } else {
2✔
1457
                        d.view.RenderInternalError(c, err)
1✔
1458
                }
1✔
1459
                return
2✔
1460
        }
1461

1462
        if deployment == nil {
6✔
1463
                d.view.RenderNoUpdateForDevice(c)
3✔
1464
                return
3✔
1465
        } else if deployment.Type == model.DeploymentTypeConfiguration {
8✔
1466
                // Generate pre-signed URL
2✔
1467
                var hostName string = d.config.PresignHostname
2✔
1468
                if hostName == "" {
3✔
1469
                        if hostName = c.Request.Header.Get(hdrForwardedHost); hostName == "" {
2✔
1470
                                d.view.RenderInternalError(c,
1✔
1471
                                        errors.New("presign.hostname not configured; "+
1✔
1472
                                                "unable to generate download link "+
1✔
1473
                                                " for configuration deployment"))
1✔
1474
                                return
1✔
1475
                        }
1✔
1476
                }
1477
                req, _ := http.NewRequest(
2✔
1478
                        http.MethodGet,
2✔
1479
                        FMTConfigURL(
2✔
1480
                                d.config.PresignScheme, hostName,
2✔
1481
                                deployment.ID, request.DeviceProvides.DeviceType,
2✔
1482
                                idata.Subject,
2✔
1483
                        ),
2✔
1484
                        nil,
2✔
1485
                )
2✔
1486
                if idata.Tenant != "" {
4✔
1487
                        q := req.URL.Query()
2✔
1488
                        q.Set(model.ParamTenantID, idata.Tenant)
2✔
1489
                        req.URL.RawQuery = q.Encode()
2✔
1490
                }
2✔
1491
                sig := model.NewRequestSignature(req, d.config.PresignSecret)
2✔
1492
                expireTS := time.Now().Add(d.config.PresignExpire)
2✔
1493
                sig.SetExpire(expireTS)
2✔
1494
                deployment.Artifact.Source = model.Link{
2✔
1495
                        Uri:    sig.PresignURL(),
2✔
1496
                        Expire: expireTS,
2✔
1497
                }
2✔
1498
        }
1499

1500
        d.view.RenderSuccessGet(c, deployment)
3✔
1501
}
1502

1503
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1504
        c *gin.Context,
1505
) {
2✔
1506
        ctx := c.Request.Context()
2✔
1507

2✔
1508
        did := c.Param("id")
2✔
1509

2✔
1510
        idata := identity.FromContext(ctx)
2✔
1511
        if idata == nil {
2✔
1512
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
1513
                return
×
1514
        }
×
1515

1516
        // receive request body
1517
        var report model.StatusReport
2✔
1518

2✔
1519
        err := c.ShouldBindJSON(&report)
2✔
1520
        if err != nil {
3✔
1521
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1522
                return
1✔
1523
        }
1✔
1524
        l := log.FromContext(ctx)
2✔
1525
        l.Infof("status: %+v", report)
2✔
1526
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
2✔
1527
                idata.Subject, model.DeviceDeploymentState{
2✔
1528
                        Status:   report.Status,
2✔
1529
                        SubState: report.SubState,
2✔
1530
                }); err != nil {
3✔
1531

1✔
1532
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
1✔
1533
                        d.view.RenderError(c, err, http.StatusConflict)
×
1534
                } else if err == app.ErrStorageNotFound {
2✔
1535
                        d.view.RenderErrorNotFound(c)
1✔
1536
                } else {
1✔
1537
                        d.view.RenderInternalError(c, err)
×
1538
                }
×
1539
                return
1✔
1540
        }
1541

1542
        d.view.RenderEmptySuccessResponse(c)
2✔
1543
}
1544

1545
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1546
        c *gin.Context,
1547
) {
2✔
1548
        ctx := c.Request.Context()
2✔
1549

2✔
1550
        did := c.Param("id")
2✔
1551

2✔
1552
        if !govalidator.IsUUID(did) {
2✔
1553
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1554
                return
×
1555
        }
×
1556

1557
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
2✔
1558
        if err != nil {
2✔
1559
                switch err {
×
1560
                case app.ErrModelDeploymentNotFound:
×
1561
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1562
                        return
×
1563
                default:
×
1564
                        d.view.RenderInternalError(c, err)
×
1565
                        return
×
1566
                }
1567
        }
1568

1569
        d.view.RenderSuccessGet(c, statuses)
2✔
1570
}
1571

1572
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1573
        c *gin.Context,
1574
) {
1✔
1575
        ctx := c.Request.Context()
1✔
1576

1✔
1577
        did := c.Param("id")
1✔
1578

1✔
1579
        if !govalidator.IsUUID(did) {
1✔
1580
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1581
                return
×
1582
        }
×
1583

1584
        page, perPage, err := rest.ParsePagingParameters(c.Request)
1✔
1585
        if err != nil {
1✔
1586
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1587
                return
×
1588
        }
×
1589

1590
        lq := store.ListQuery{
1✔
1591
                Skip:         int((page - 1) * perPage),
1✔
1592
                Limit:        int(perPage),
1✔
1593
                DeploymentID: did,
1✔
1594
        }
1✔
1595
        if status := c.Request.URL.Query().Get("status"); status != "" {
1✔
1596
                lq.Status = &status
×
1597
        }
×
1598
        if err = lq.Validate(); err != nil {
1✔
1599
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1600
                return
×
1601
        }
×
1602

1603
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1604
        if err != nil {
1✔
1605
                switch err {
×
1606
                case app.ErrModelDeploymentNotFound:
×
1607
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1608
                        return
×
1609
                default:
×
1610
                        d.view.RenderInternalError(c, err)
×
1611
                        return
×
1612
                }
1613
        }
1614

1615
        hasNext := totalCount > int(page*perPage)
1✔
1616
        hints := rest.NewPagingHints().
1✔
1617
                SetPage(page).
1✔
1618
                SetPerPage(perPage).
1✔
1619
                SetHasNext(hasNext).
1✔
1620
                SetTotalCount(int64(totalCount))
1✔
1621

1✔
1622
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
1623
        if err != nil {
1✔
1624
                d.view.RenderInternalError(c, err)
×
1625
                return
×
1626
        }
×
1627

1628
        for _, l := range links {
2✔
1629
                c.Writer.Header().Add(hdrLink, l)
1✔
1630
        }
1✔
1631
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1632
        d.view.RenderSuccessGet(c, statuses)
1✔
1633
}
1634

1635
func ParseLookupQuery(vals url.Values) (model.Query, error) {
3✔
1636
        query := model.Query{}
3✔
1637

3✔
1638
        createdBefore := vals.Get("created_before")
3✔
1639
        if createdBefore != "" {
5✔
1640
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
3✔
1641
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1642
                } else {
2✔
1643
                        query.CreatedBefore = &createdBeforeTime
1✔
1644
                }
1✔
1645
        }
1646

1647
        createdAfter := vals.Get("created_after")
3✔
1648
        if createdAfter != "" {
4✔
1649
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
1✔
1650
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1651
                } else {
1✔
1652
                        query.CreatedAfter = &createdAfterTime
1✔
1653
                }
1✔
1654
        }
1655

1656
        switch strings.ToLower(vals.Get("sort")) {
3✔
1657
        case model.SortDirectionAscending:
1✔
1658
                query.Sort = model.SortDirectionAscending
1✔
1659
        case "", model.SortDirectionDescending:
3✔
1660
                query.Sort = model.SortDirectionDescending
3✔
1661
        default:
×
1662
                return query, ErrInvalidSortDirection
×
1663
        }
1664

1665
        status := vals.Get("status")
3✔
1666
        switch status {
3✔
1667
        case "inprogress":
×
1668
                query.Status = model.StatusQueryInProgress
×
1669
        case "finished":
×
1670
                query.Status = model.StatusQueryFinished
×
1671
        case "pending":
×
1672
                query.Status = model.StatusQueryPending
×
1673
        case "aborted":
×
1674
                query.Status = model.StatusQueryAborted
×
1675
        case "":
3✔
1676
                query.Status = model.StatusQueryAny
3✔
1677
        default:
×
1678
                return query, errors.Errorf("unknown status %s", status)
×
1679

1680
        }
1681

1682
        dType := vals.Get("type")
3✔
1683
        if dType == "" {
6✔
1684
                return query, nil
3✔
1685
        }
3✔
1686
        deploymentType := model.DeploymentType(dType)
×
1687
        if deploymentType == model.DeploymentTypeSoftware ||
×
1688
                deploymentType == model.DeploymentTypeConfiguration {
×
1689
                query.Type = deploymentType
×
1690
        } else {
×
1691
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1692
        }
×
1693

1694
        return query, nil
×
1695
}
1696

1697
func ParseDeploymentLookupQueryV1(vals url.Values) (model.Query, error) {
3✔
1698
        query, err := ParseLookupQuery(vals)
3✔
1699
        if err != nil {
4✔
1700
                return query, err
1✔
1701
        }
1✔
1702

1703
        search := vals.Get("search")
3✔
1704
        if search != "" {
3✔
1705
                query.SearchText = search
×
1706
        }
×
1707

1708
        return query, nil
3✔
1709
}
1710

1711
func ParseDeploymentLookupQueryV2(vals url.Values) (model.Query, error) {
2✔
1712
        query, err := ParseLookupQuery(vals)
2✔
1713
        if err != nil {
2✔
1714
                return query, err
×
1715
        }
×
1716

1717
        query.Names = vals["name"]
2✔
1718
        query.IDs = vals["id"]
2✔
1719

2✔
1720
        return query, nil
2✔
1721
}
1722

1723
func parseEpochToTimestamp(epoch string) (time.Time, error) {
2✔
1724
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
3✔
1725
                return time.Time{}, errors.New("invalid timestamp: " + epoch)
1✔
1726
        } else {
2✔
1727
                return time.Unix(epochInt64, 0).UTC(), nil
1✔
1728
        }
1✔
1729
}
1730

1731
func (d *DeploymentsApiHandlers) LookupDeployment(c *gin.Context) {
3✔
1732
        ctx := c.Request.Context()
3✔
1733
        q := c.Request.URL.Query()
3✔
1734
        defer func() {
6✔
1735
                if search := q.Get("search"); search != "" {
3✔
1736
                        q.Set("search", Redacted)
×
1737
                        c.Request.URL.RawQuery = q.Encode()
×
1738
                }
×
1739
        }()
1740

1741
        query, err := ParseDeploymentLookupQueryV1(q)
3✔
1742
        if err != nil {
4✔
1743
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1744
                return
1✔
1745
        }
1✔
1746

1747
        page, perPage, err := rest.ParsePagingParameters(c.Request)
3✔
1748
        if err != nil {
4✔
1749
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1750
                return
1✔
1751
        }
1✔
1752
        query.Skip = int((page - 1) * perPage)
3✔
1753
        query.Limit = int(perPage + 1)
3✔
1754

3✔
1755
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
3✔
1756
        if err != nil {
4✔
1757
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1758
                return
1✔
1759
        }
1✔
1760
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
3✔
1761

3✔
1762
        len := len(deps)
3✔
1763
        hasNext := false
3✔
1764
        if int64(len) > perPage {
3✔
1765
                hasNext = true
×
1766
                len = int(perPage)
×
1767
        }
×
1768

1769
        hints := rest.NewPagingHints().
3✔
1770
                SetPage(page).
3✔
1771
                SetPerPage(perPage).
3✔
1772
                SetHasNext(hasNext).
3✔
1773
                SetTotalCount(int64(totalCount))
3✔
1774

3✔
1775
        links, err := rest.MakePagingHeaders(c.Request, hints)
3✔
1776
        if err != nil {
3✔
1777
                d.view.RenderInternalError(c, err)
×
1778
                return
×
1779
        }
×
1780
        for _, l := range links {
6✔
1781
                c.Writer.Header().Add(hdrLink, l)
3✔
1782
        }
3✔
1783

1784
        d.view.RenderSuccessGet(c, deps[:len])
3✔
1785
}
1786

1787
func (d *DeploymentsApiHandlers) LookupDeploymentV2(c *gin.Context) {
2✔
1788
        ctx := c.Request.Context()
2✔
1789
        q := c.Request.URL.Query()
2✔
1790
        defer func() {
4✔
1791
                if q.Has("name") {
3✔
1792
                        q["name"] = []string{Redacted}
1✔
1793
                        c.Request.URL.RawQuery = q.Encode()
1✔
1794
                }
1✔
1795
        }()
1796

1797
        query, err := ParseDeploymentLookupQueryV2(q)
2✔
1798
        if err != nil {
2✔
1799
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1800
                return
×
1801
        }
×
1802

1803
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1804
        if err != nil {
3✔
1805
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1806
                return
1✔
1807
        }
1✔
1808
        query.Skip = int((page - 1) * perPage)
2✔
1809
        query.Limit = int(perPage + 1)
2✔
1810

2✔
1811
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
2✔
1812
        if err != nil {
2✔
1813
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1814
                return
×
1815
        }
×
1816
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
2✔
1817

2✔
1818
        len := len(deps)
2✔
1819
        hasNext := false
2✔
1820
        if int64(len) > perPage {
3✔
1821
                hasNext = true
1✔
1822
                len = int(perPage)
1✔
1823
        }
1✔
1824

1825
        hints := rest.NewPagingHints().
2✔
1826
                SetPage(page).
2✔
1827
                SetPerPage(perPage).
2✔
1828
                SetHasNext(hasNext).
2✔
1829
                SetTotalCount(int64(totalCount))
2✔
1830

2✔
1831
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
1832
        if err != nil {
2✔
1833
                d.view.RenderInternalError(c, err)
×
1834
                return
×
1835
        }
×
1836
        for _, l := range links {
4✔
1837
                c.Writer.Header().Add(hdrLink, l)
2✔
1838
        }
2✔
1839

1840
        d.view.RenderSuccessGet(c, deps[:len])
2✔
1841
}
1842

1843
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(c *gin.Context) {
1✔
1844
        ctx := c.Request.Context()
1✔
1845

1✔
1846
        did := c.Param("id")
1✔
1847

1✔
1848
        idata := identity.FromContext(ctx)
1✔
1849
        if idata == nil {
1✔
1850
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
1851
                return
×
1852
        }
×
1853

1854
        // reuse DeploymentLog, device and deployment IDs are ignored when
1855
        // (un-)marshaling DeploymentLog to/from JSON
1856
        var log model.DeploymentLog
1✔
1857

1✔
1858
        err := c.ShouldBindJSON(&log)
1✔
1859
        if err != nil {
1✔
1860
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1861
                return
×
1862
        }
×
1863

1864
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1865
                did, log.Messages); err != nil {
1✔
1866

×
1867
                if err == app.ErrModelDeploymentNotFound {
×
1868
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1869
                } else {
×
1870
                        d.view.RenderInternalError(c, err)
×
1871
                }
×
1872
                return
×
1873
        }
1874

1875
        d.view.RenderEmptySuccessResponse(c)
1✔
1876
}
1877

1878
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(c *gin.Context) {
1✔
1879
        ctx := c.Request.Context()
1✔
1880

1✔
1881
        did := c.Param("id")
1✔
1882
        devid := c.Param("devid")
1✔
1883

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

1✔
1886
        if err != nil {
1✔
1887
                d.view.RenderInternalError(c, err)
×
1888
                return
×
1889
        }
×
1890

1891
        if depl == nil {
1✔
1892
                d.view.RenderErrorNotFound(c)
×
1893
                return
×
1894
        }
×
1895

1896
        d.view.RenderDeploymentLog(c, *depl)
1✔
1897
}
1898

1899
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(c *gin.Context) {
1✔
1900
        ctx := c.Request.Context()
1✔
1901

1✔
1902
        id := c.Param("id")
1✔
1903
        err := d.app.AbortDeviceDeployments(ctx, id)
1✔
1904

1✔
1905
        switch err {
1✔
1906
        case nil, app.ErrStorageNotFound:
1✔
1907
                d.view.RenderEmptySuccessResponse(c)
1✔
1908
        default:
1✔
1909
                d.view.RenderInternalError(c, err)
1✔
1910
        }
1911
}
1912

1913
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(c *gin.Context) {
1✔
1914
        ctx := c.Request.Context()
1✔
1915

1✔
1916
        id := c.Param("id")
1✔
1917
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
1✔
1918

1✔
1919
        switch err {
1✔
1920
        case nil, app.ErrStorageNotFound:
1✔
1921
                d.view.RenderEmptySuccessResponse(c)
1✔
1922
        default:
1✔
1923
                d.view.RenderInternalError(c, err)
1✔
1924
        }
1925
}
1926

1927
func (d *DeploymentsApiHandlers) ListDeviceDeployments(c *gin.Context) {
2✔
1928
        ctx := c.Request.Context()
2✔
1929
        d.listDeviceDeployments(ctx, c, true)
2✔
1930
}
2✔
1931

1932
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(c *gin.Context) {
2✔
1933
        ctx := c.Request.Context()
2✔
1934
        tenantID := c.Param("tenant")
2✔
1935
        if tenantID != "" {
4✔
1936
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
1937
                        Tenant:   tenantID,
2✔
1938
                        IsDevice: true,
2✔
1939
                })
2✔
1940
        }
2✔
1941
        d.listDeviceDeployments(ctx, c, true)
2✔
1942
}
1943

1944
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(c *gin.Context) {
2✔
1945
        ctx := c.Request.Context()
2✔
1946
        tenantID := c.Param("tenant")
2✔
1947
        if tenantID != "" {
4✔
1948
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
1949
                        Tenant:   tenantID,
2✔
1950
                        IsDevice: true,
2✔
1951
                })
2✔
1952
        }
2✔
1953
        d.listDeviceDeployments(ctx, c, false)
2✔
1954
}
1955

1956
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1957
        c *gin.Context, byDeviceID bool) {
2✔
1958

2✔
1959
        did := ""
2✔
1960
        var IDs []string
2✔
1961
        if byDeviceID {
4✔
1962
                did = c.Param("id")
2✔
1963
        } else {
4✔
1964
                values := c.Request.URL.Query()
2✔
1965
                if values.Has("id") && len(values["id"]) > 0 {
3✔
1966
                        IDs = values["id"]
1✔
1967
                } else {
3✔
1968
                        d.view.RenderError(c, ErrEmptyID, http.StatusBadRequest)
2✔
1969
                        return
2✔
1970
                }
2✔
1971
        }
1972

1973
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1974
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
3✔
1975
                err = rest.ErrQueryParmLimit(ParamPerPage)
1✔
1976
        }
1✔
1977
        if err != nil {
3✔
1978
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1979
                return
1✔
1980
        }
1✔
1981

1982
        lq := store.ListQueryDeviceDeployments{
2✔
1983
                Skip:     int((page - 1) * perPage),
2✔
1984
                Limit:    int(perPage),
2✔
1985
                DeviceID: did,
2✔
1986
                IDs:      IDs,
2✔
1987
        }
2✔
1988
        if status := c.Request.URL.Query().Get("status"); status != "" {
3✔
1989
                lq.Status = &status
1✔
1990
        }
1✔
1991
        if err = lq.Validate(); err != nil {
3✔
1992
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1993
                return
1✔
1994
        }
1✔
1995

1996
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
2✔
1997
        if err != nil {
3✔
1998
                d.view.RenderInternalError(c, err)
1✔
1999
                return
1✔
2000
        }
1✔
2001
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
2✔
2002

2✔
2003
        hasNext := totalCount > lq.Skip+len(deps)
2✔
2004

2✔
2005
        hints := rest.NewPagingHints().
2✔
2006
                SetPage(page).
2✔
2007
                SetPerPage(perPage).
2✔
2008
                SetHasNext(hasNext).
2✔
2009
                SetTotalCount(int64(totalCount))
2✔
2010

2✔
2011
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
2012
        if err != nil {
2✔
2013
                rest.RenderInternalError(c, err)
×
2014
                return
×
2015
        }
×
2016
        for _, l := range links {
4✔
2017
                c.Writer.Header().Add(hdrLink, l)
2✔
2018
        }
2✔
2019

2020
        d.view.RenderSuccessGet(c, deps)
2✔
2021
}
2022

2023
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(c *gin.Context) {
1✔
2024
        ctx := c.Request.Context()
1✔
2025
        tenantID := c.Param("tenantID")
1✔
2026
        if tenantID != "" {
1✔
2027
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
×
2028
                        Tenant:   tenantID,
×
2029
                        IsDevice: true,
×
2030
                })
×
2031
        }
×
2032

2033
        id := c.Param("id")
1✔
2034

1✔
2035
        // Decommission deployments for devices and update deployment stats
1✔
2036
        err := d.app.DecommissionDevice(ctx, id)
1✔
2037

1✔
2038
        switch err {
1✔
2039
        case nil, app.ErrStorageNotFound:
1✔
2040
                d.view.RenderEmptySuccessResponse(c)
1✔
2041
        default:
×
2042
                d.view.RenderInternalError(c, err)
×
2043

2044
        }
2045
}
2046

2047
// tenants
2048

2049
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(c *gin.Context) {
2✔
2050
        ctx := c.Request.Context()
2✔
2051

2✔
2052
        defer c.Request.Body.Close()
2✔
2053

2✔
2054
        tenant, err := model.ParseNewTenantReq(c.Request.Body)
2✔
2055
        if err != nil {
4✔
2056
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
2057
                return
2✔
2058
        }
2✔
2059

2060
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
2061
        if err != nil {
1✔
2062
                d.view.RenderInternalError(c, err)
×
2063
                return
×
2064
        }
×
2065

2066
        c.Status(http.StatusCreated)
1✔
2067
}
2068

2069
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
2070
        c *gin.Context,
2071
) {
2✔
2072
        tenantID := c.Param("tenant")
2✔
2073
        if tenantID == "" {
3✔
2074

1✔
2075
                d.view.RenderError(c, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
2076
                return
1✔
2077
        }
1✔
2078
        c.Request = c.Request.WithContext(identity.WithContext(
2✔
2079
                c.Request.Context(),
2✔
2080
                &identity.Identity{Tenant: tenantID},
2✔
2081
        ))
2✔
2082
        d.LookupDeployment(c)
2✔
2083
}
2084

2085
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
2086
        c *gin.Context,
2087
) {
3✔
2088

3✔
2089
        tenantID := c.Param("tenant")
3✔
2090

3✔
2091
        ctx := identity.WithContext(
3✔
2092
                c.Request.Context(),
3✔
2093
                &identity.Identity{Tenant: tenantID},
3✔
2094
        )
3✔
2095

3✔
2096
        settings, err := d.app.GetStorageSettings(ctx)
3✔
2097
        if err != nil {
4✔
2098
                d.view.RenderInternalError(c, err)
1✔
2099
                return
1✔
2100
        }
1✔
2101

2102
        d.view.RenderSuccessGet(c, settings)
3✔
2103
}
2104

2105
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
2106
        c *gin.Context,
2107
) {
3✔
2108

3✔
2109
        defer c.Request.Body.Close()
3✔
2110

3✔
2111
        tenantID := c.Param("tenant")
3✔
2112

3✔
2113
        ctx := identity.WithContext(
3✔
2114
                c.Request.Context(),
3✔
2115
                &identity.Identity{Tenant: tenantID},
3✔
2116
        )
3✔
2117

3✔
2118
        settings, err := model.ParseStorageSettingsRequest(c.Request.Body)
3✔
2119
        if err != nil {
6✔
2120
                d.view.RenderError(c, err, http.StatusBadRequest)
3✔
2121
                return
3✔
2122
        }
3✔
2123

2124
        err = d.app.SetStorageSettings(ctx, settings)
2✔
2125
        if err != nil {
3✔
2126
                d.view.RenderInternalError(c, err)
1✔
2127
                return
1✔
2128
        }
1✔
2129

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