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

mendersoftware / mender-server / 1927764361

15 Jul 2025 11:39AM UTC coverage: 65.436% (-0.02%) from 65.454%
1927764361

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

139 of 232 new or added lines in 7 files covered. (59.91%)

77 existing lines in 3 files now uncovered.

32212 of 49227 relevant lines covered (65.44%)

1.39 hits per line

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

79.11
/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
        EnableDirectUpload bool
142
        // EnableDirectUploadSkipVerify allows turning off the verification of uploaded artifacts
143
        EnableDirectUploadSkipVerify bool
144

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

151
func NewConfig() *Config {
3✔
152
        return &Config{
3✔
153
                PresignExpire:       DefaultDownloadLinkExpire,
3✔
154
                PresignScheme:       "https",
3✔
155
                MaxImageSize:        DefaultMaxImageSize,
3✔
156
                MaxGenerateDataSize: DefaultMaxGenerateDataSize,
3✔
157
        }
3✔
158
}
3✔
159

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

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

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

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

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

185
func (conf *Config) SetMaxGenerateDataSize(size int64) *Config {
2✔
186
        conf.MaxGenerateDataSize = size
2✔
187
        return conf
2✔
188
}
2✔
189

190
func (conf *Config) SetEnableDirectUpload(enable bool) *Config {
3✔
191
        conf.EnableDirectUpload = enable
3✔
192
        return conf
3✔
193
}
3✔
194

195
func (conf *Config) SetEnableDirectUploadSkipVerify(enable bool) *Config {
2✔
196
        conf.EnableDirectUploadSkipVerify = enable
2✔
197
        return conf
2✔
198
}
2✔
199

200
func (conf *Config) SetDisableNewReleasesFeature(disable bool) *Config {
3✔
201
        conf.DisableNewReleasesFeature = disable
3✔
202
        return conf
3✔
203
}
3✔
204

205
type DeploymentsApiHandlers struct {
206
        view   RESTView
207
        store  store.DataStore
208
        app    app.App
209
        config Config
210
}
211

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

253
func (d *DeploymentsApiHandlers) AliveHandler(c *gin.Context) {
2✔
254
        c.Status(http.StatusNoContent)
2✔
255
}
2✔
256

257
func (d *DeploymentsApiHandlers) HealthHandler(c *gin.Context) {
2✔
258
        ctx := c.Request.Context()
2✔
259
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
2✔
260
        defer cancel()
2✔
261

2✔
262
        err := d.app.HealthCheck(ctx)
2✔
263
        if err != nil {
3✔
264
                d.view.RenderError(c, err, http.StatusServiceUnavailable)
1✔
265
                return
1✔
266
        }
1✔
267
        c.Status(http.StatusNoContent)
2✔
268
}
269

270
func getReleaseOrImageFilter(r *http.Request, version listReleasesVersion,
271
        paginated bool) *model.ReleaseOrImageFilter {
3✔
272

3✔
273
        q := r.URL.Query()
3✔
274

3✔
275
        filter := &model.ReleaseOrImageFilter{
3✔
276
                Name:       q.Get(ParamName),
3✔
277
                UpdateType: q.Get(ParamUpdateType),
3✔
278
        }
3✔
279
        if version == listReleasesV1 {
6✔
280
                filter.Description = q.Get(ParamDescription)
3✔
281
                filter.DeviceType = q.Get(ParamDeviceType)
3✔
282
        } else if version == listReleasesV2 {
5✔
283
                filter.Tags = q[ParamTag]
1✔
284
                for i, t := range filter.Tags {
2✔
285
                        filter.Tags[i] = strings.ToLower(t)
1✔
286
                }
1✔
287
        }
288

289
        if paginated {
5✔
290
                filter.Sort = q.Get(ParamSort)
2✔
291
                if page := q.Get(ParamPage); page != "" {
3✔
292
                        if i, err := strconv.Atoi(page); err == nil {
2✔
293
                                filter.Page = i
1✔
294
                        }
1✔
295
                }
296
                if perPage := q.Get(ParamPerPage); perPage != "" {
3✔
297
                        if i, err := strconv.Atoi(perPage); err == nil {
2✔
298
                                filter.PerPage = i
1✔
299
                        }
1✔
300
                }
301
                if filter.Page <= 0 {
4✔
302
                        filter.Page = 1
2✔
303
                }
2✔
304
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
4✔
305
                        filter.PerPage = DefaultPerPage
2✔
306
                }
2✔
307
        }
308

309
        return filter
3✔
310
}
311

312
func getImageFilter(c *gin.Context, paginated bool) *model.ImageFilter {
1✔
313

1✔
314
        q := c.Request.URL.Query()
1✔
315

1✔
316
        names := c.QueryArray(ParamName)
1✔
317

1✔
318
        var exactNames []string
1✔
319
        var nameprefix string
1✔
320

1✔
321
        for _, name := range names {
2✔
322
                if strings.HasSuffix(name, `\*`) {
1✔
NEW
UNCOV
323
                        name = strings.TrimSuffix(name, `\*`) + "*"
×
NEW
324
                        exactNames = append(exactNames, name)
×
325
                } else if strings.HasSuffix(name, "*") {
1✔
NEW
326
                        nameprefix = strings.TrimSuffix(name, "*")
×
327
                } else {
1✔
328
                        exactNames = append(exactNames, name)
1✔
329
                }
1✔
330
        }
331

332
        filter := &model.ImageFilter{
1✔
333
                ExactNames:  exactNames,
1✔
334
                NamePrefix:  nameprefix,
1✔
335
                Description: q.Get(ParamDescription),
1✔
336
                DeviceType:  q.Get(ParamDeviceType),
1✔
337
        }
1✔
338

1✔
339
        if paginated {
2✔
340
                // filter.Sort = q.Get(ParamSort)
1✔
341
                if page := q.Get(ParamPage); page != "" {
1✔
NEW
342
                        if i, err := strconv.Atoi(page); err == nil {
×
NEW
343
                                filter.Page = i
×
NEW
344
                        }
×
345
                }
346
                if perPage := q.Get(ParamPerPage); perPage != "" {
1✔
NEW
347
                        if i, err := strconv.Atoi(perPage); err == nil {
×
NEW
348
                                filter.PerPage = i
×
NEW
349
                        }
×
350
                }
351
                if filter.Page <= 0 {
2✔
352
                        filter.Page = 1
1✔
353
                }
1✔
354
                if filter.PerPage <= 0 || filter.PerPage > MaximumPerPage {
2✔
355
                        filter.PerPage = DefaultPerPage
1✔
356
                }
1✔
357
        }
358

359
        return filter
1✔
360
}
361

362
type limitResponse struct {
363
        Limit uint64 `json:"limit"`
364
        Usage uint64 `json:"usage"`
365
}
366

367
func (d *DeploymentsApiHandlers) GetLimit(c *gin.Context) {
1✔
368

1✔
369
        name := c.Param("name")
1✔
370

1✔
371
        if !model.IsValidLimit(name) {
2✔
372
                d.view.RenderError(c,
1✔
373
                        errors.Errorf("unsupported limit %s", name),
1✔
374
                        http.StatusBadRequest)
1✔
375
                return
1✔
376
        }
1✔
377

378
        limit, err := d.app.GetLimit(c.Request.Context(), name)
1✔
379
        if err != nil {
2✔
380
                d.view.RenderInternalError(c, err)
1✔
381
                return
1✔
382
        }
1✔
383

384
        d.view.RenderSuccessGet(c, limitResponse{
1✔
385
                Limit: limit.Value,
1✔
386
                Usage: 0, // TODO fill this when ready
1✔
387
        })
1✔
388
}
389

390
// images
391

392
func (d *DeploymentsApiHandlers) GetImage(c *gin.Context) {
2✔
393

2✔
394
        id := c.Param("id")
2✔
395

2✔
396
        if !govalidator.IsUUID(id) {
3✔
397
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
398
                return
1✔
399
        }
1✔
400

401
        image, err := d.app.GetImage(c.Request.Context(), id)
2✔
402
        if err != nil {
2✔
403
                d.view.RenderInternalError(c, err)
×
404
                return
×
405
        }
×
406

407
        if image == nil {
3✔
408
                d.view.RenderErrorNotFound(c)
1✔
409
                return
1✔
410
        }
1✔
411

412
        d.view.RenderSuccessGet(c, image)
2✔
413
}
414

415
func (d *DeploymentsApiHandlers) GetImages(c *gin.Context) {
3✔
416

3✔
417
        defer redactReleaseName(c.Request)
3✔
418
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, false)
3✔
419

3✔
420
        list, _, err := d.app.ListImages(c.Request.Context(), filter)
3✔
421
        if err != nil {
4✔
422
                d.view.RenderInternalError(c, err)
1✔
423
                return
1✔
424
        }
1✔
425

426
        d.view.RenderSuccessGet(c, list)
3✔
427
}
428

429
func (d *DeploymentsApiHandlers) ListImages(c *gin.Context) {
1✔
430

1✔
431
        defer redactReleaseName(c.Request)
1✔
432
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, true)
1✔
433

1✔
434
        list, totalCount, err := d.app.ListImages(c.Request.Context(), filter)
1✔
435
        if err != nil {
2✔
436
                d.view.RenderInternalError(c, err)
1✔
437
                return
1✔
438
        }
1✔
439

440
        hasNext := totalCount > int(filter.Page*filter.PerPage)
1✔
441

1✔
442
        hints := rest.NewPagingHints().
1✔
443
                SetPage(int64(filter.Page)).
1✔
444
                SetPerPage(int64(filter.PerPage)).
1✔
445
                SetHasNext(hasNext).
1✔
446
                SetTotalCount(int64(totalCount))
1✔
447

1✔
448
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
449
        if err != nil {
1✔
450
                d.view.RenderInternalError(c, err)
×
451
                return
×
452
        }
×
453

454
        for _, l := range links {
2✔
455
                c.Writer.Header().Add(hdrLink, l)
1✔
456
        }
1✔
457
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
458

1✔
459
        d.view.RenderSuccessGet(c, list)
1✔
460
}
461

462
func (d *DeploymentsApiHandlers) ListImagesV2(c *gin.Context) {
1✔
463

1✔
464
        defer redactReleaseName(c.Request)
1✔
465
        filter := getImageFilter(c, true)
1✔
466

1✔
467
        if err := filter.Validate(); err != nil {
1✔
NEW
468
                d.view.RenderError(c, errors.Wrap(err, "invalid filter"), http.StatusBadRequest)
×
NEW
469
                return
×
NEW
UNCOV
470
        }
×
471

472
        // add one more item to the limit to check if there is a next page.
473
        filter.Limit = filter.PerPage + 1
1✔
474

1✔
475
        list, err := d.app.ListImagesV2(c.Request.Context(), filter)
1✔
476
        if err != nil {
2✔
477
                d.view.RenderInternalError(c, err)
1✔
478
                return
1✔
479
        }
1✔
480

481
        length := len(list)
1✔
482
        hasNext := false
1✔
483
        if length > filter.PerPage {
1✔
NEW
484
                hasNext = true
×
NEW
485
                length = filter.PerPage
×
NEW
486
        }
×
487

488
        hints := rest.NewPagingHints().
1✔
489
                SetPage(int64(filter.Page)).
1✔
490
                SetPerPage(int64(filter.PerPage)).
1✔
491
                SetHasNext(hasNext)
1✔
492

1✔
493
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
494
        if err != nil {
1✔
NEW
495
                d.view.RenderInternalError(c, err)
×
NEW
496
                return
×
NEW
497
        }
×
498

499
        for _, l := range links {
2✔
500
                c.Writer.Header().Add(hdrLink, l)
1✔
501
        }
1✔
502

503
        d.view.RenderSuccessGet(c, list[:length])
1✔
504
}
505

506
func (d *DeploymentsApiHandlers) DownloadLink(c *gin.Context) {
1✔
507

1✔
508
        id := c.Param("id")
1✔
509

1✔
510
        if !govalidator.IsUUID(id) {
1✔
511
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
512
                return
×
513
        }
×
514

515
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
516
        link, err := d.app.DownloadLink(c.Request.Context(), id,
1✔
517
                time.Duration(expireSeconds)*time.Second)
1✔
518
        if err != nil {
1✔
519
                d.view.RenderInternalError(c, err)
×
520
                return
×
521
        }
×
522

523
        if link == nil {
1✔
524
                d.view.RenderErrorNotFound(c)
×
525
                return
×
526
        }
×
527

528
        d.view.RenderSuccessGet(c, link)
1✔
529
}
530

531
func (d *DeploymentsApiHandlers) UploadLink(c *gin.Context) {
2✔
532

2✔
533
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
2✔
534
        link, err := d.app.UploadLink(
2✔
535
                c.Request.Context(),
2✔
536
                time.Duration(expireSeconds)*time.Second,
2✔
537
                d.config.EnableDirectUploadSkipVerify,
2✔
538
        )
2✔
539
        if err != nil {
3✔
540
                d.view.RenderInternalError(c, err)
1✔
541
                return
1✔
542
        }
1✔
543

544
        if link == nil {
3✔
545
                d.view.RenderErrorNotFound(c)
1✔
546
                return
1✔
547
        }
1✔
548

549
        d.view.RenderSuccessGet(c, link)
2✔
550
}
551

552
const maxMetadataSize = 2048
553

554
func (d *DeploymentsApiHandlers) CompleteUpload(c *gin.Context) {
2✔
555
        ctx := c.Request.Context()
2✔
556
        l := log.FromContext(ctx)
2✔
557

2✔
558
        artifactID := c.Param(ParamID)
2✔
559

2✔
560
        var metadata *model.DirectUploadMetadata
2✔
561
        if d.config.EnableDirectUploadSkipVerify {
3✔
562
                var directMetadata model.DirectUploadMetadata
1✔
563
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
564
                n, err := io.ReadFull(c.Request.Body, bodyBuffer)
1✔
565
                c.Request.Body.Close()
1✔
566
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
567
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
568
                } else {
1✔
569
                        err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
570
                        if err == nil {
2✔
571
                                if directMetadata.Validate() == nil {
2✔
572
                                        metadata = &directMetadata
1✔
573
                                }
1✔
574
                        } else {
1✔
575
                                l.Errorf("error parsing json data: %s", err.Error())
1✔
576
                        }
1✔
577
                }
578
        }
579

580
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
2✔
581
        switch errors.Cause(err) {
2✔
582
        case nil:
2✔
583
                // c.Writer.Header().Set("Link", "FEAT: Upload status API")
2✔
584
                c.Status(http.StatusAccepted)
2✔
585
        case app.ErrUploadNotFound:
1✔
586
                d.view.RenderErrorNotFound(c)
1✔
587
        default:
1✔
588
                d.view.RenderInternalError(c, err)
1✔
589
        }
590
}
591

592
func (d *DeploymentsApiHandlers) DownloadConfiguration(c *gin.Context) {
3✔
593
        if d.config.PresignSecret == nil {
4✔
594
                d.view.RenderErrorNotFound(c)
1✔
595
                return
1✔
596
        }
1✔
597
        var (
3✔
598
                deviceID, _     = url.PathUnescape(c.Param(ParamDeviceID))
3✔
599
                deviceType, _   = url.PathUnescape(c.Param(ParamDeviceType))
3✔
600
                deploymentID, _ = url.PathUnescape(c.Param(ParamDeploymentID))
3✔
601
        )
3✔
602
        if deviceID == "" || deviceType == "" || deploymentID == "" {
3✔
603
                d.view.RenderErrorNotFound(c)
×
604
                return
×
605
        }
×
606

607
        var (
3✔
608
                tenantID string
3✔
609
                q        = c.Request.URL.Query()
3✔
610
                err      error
3✔
611
        )
3✔
612
        tenantID = q.Get(ParamTenantID)
3✔
613
        sig := model.NewRequestSignature(c.Request, d.config.PresignSecret)
3✔
614
        if err = sig.Validate(); err != nil {
6✔
615
                switch cause := errors.Cause(err); cause {
3✔
616
                case model.ErrLinkExpired:
1✔
617
                        d.view.RenderError(c, cause, http.StatusForbidden)
1✔
618
                default:
3✔
619
                        d.view.RenderError(c,
3✔
620
                                errors.Wrap(err, "invalid request parameters"),
3✔
621
                                http.StatusBadRequest,
3✔
622
                        )
3✔
623
                }
624
                return
3✔
625
        }
626

627
        if !sig.VerifyHMAC256() {
4✔
628
                d.view.RenderError(c,
2✔
629
                        errors.New("signature invalid"),
2✔
630
                        http.StatusForbidden,
2✔
631
                )
2✔
632
                return
2✔
633
        }
2✔
634

635
        // Validate request signature
636
        ctx := identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
637
                Subject:  deviceID,
2✔
638
                Tenant:   tenantID,
2✔
639
                IsDevice: true,
2✔
640
        })
2✔
641

2✔
642
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
2✔
643
        if err != nil {
3✔
644
                switch cause := errors.Cause(err); cause {
1✔
645
                case app.ErrModelDeploymentNotFound:
1✔
646
                        d.view.RenderError(c,
1✔
647
                                errors.Errorf(
1✔
648
                                        "deployment with id '%s' not found",
1✔
649
                                        deploymentID,
1✔
650
                                ),
1✔
651
                                http.StatusNotFound,
1✔
652
                        )
1✔
653
                default:
1✔
654
                        d.view.RenderInternalError(c, err)
1✔
655
                }
656
                return
1✔
657
        }
658
        artifactPayload, err := io.ReadAll(artifact)
2✔
659
        if err != nil {
3✔
660
                d.view.RenderInternalError(c, err)
1✔
661
                return
1✔
662
        }
1✔
663

664
        rw := c.Writer
2✔
665
        hdr := rw.Header()
2✔
666
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
2✔
667
        hdr.Set("Content-Type", app.ArtifactContentType)
2✔
668
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
2✔
669
        c.Status(http.StatusOK)
2✔
670
        _, err = rw.Write(artifactPayload)
2✔
671
        if err != nil {
2✔
672
                // There's not anything we can do here in terms of the response.
×
673
                _ = c.Error(err)
×
674
        }
×
675
}
676

677
func (d *DeploymentsApiHandlers) DeleteImage(c *gin.Context) {
1✔
678

1✔
679
        id := c.Param("id")
1✔
680

1✔
681
        if !govalidator.IsUUID(id) {
1✔
682
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
683
                return
×
684
        }
×
685

686
        if err := d.app.DeleteImage(c.Request.Context(), id); err != nil {
2✔
687
                switch err {
1✔
688
                default:
×
689
                        d.view.RenderInternalError(c, err)
×
690
                case app.ErrImageMetaNotFound:
×
691
                        d.view.RenderErrorNotFound(c)
×
692
                case app.ErrModelImageInActiveDeployment:
1✔
693
                        d.view.RenderError(c, ErrArtifactUsedInActiveDeployment, http.StatusConflict)
1✔
694
                }
695
                return
1✔
696
        }
697

698
        d.view.RenderSuccessDelete(c)
1✔
699
}
700

701
func (d *DeploymentsApiHandlers) EditImage(c *gin.Context) {
×
702

×
703
        id := c.Param("id")
×
704

×
705
        if !govalidator.IsUUID(id) {
×
706
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
707
                return
×
708
        }
×
709

710
        constructor, err := getImageMetaFromBody(c)
×
711
        if err != nil {
×
712
                d.view.RenderError(
×
713
                        c,
×
714
                        errors.Wrap(err, "Validating request body"),
×
715
                        http.StatusBadRequest,
×
716
                )
×
717
                return
×
718
        }
×
719

720
        found, err := d.app.EditImage(c.Request.Context(), id, constructor)
×
721
        if err != nil {
×
722
                if err == app.ErrModelImageUsedInAnyDeployment {
×
723
                        d.view.RenderError(c, err, http.StatusUnprocessableEntity)
×
724
                        return
×
725
                }
×
726
                d.view.RenderInternalError(c, err)
×
727
                return
×
728
        }
729

730
        if !found {
×
731
                d.view.RenderErrorNotFound(c)
×
732
                return
×
733
        }
×
734

735
        d.view.RenderSuccessPut(c)
×
736
}
737

738
func getImageMetaFromBody(c *gin.Context) (*model.ImageMeta, error) {
×
739

×
740
        var constructor *model.ImageMeta
×
741

×
742
        if err := c.ShouldBindJSON(&constructor); err != nil {
×
743
                return nil, err
×
744
        }
×
745

746
        if err := constructor.Validate(); err != nil {
×
747
                return nil, err
×
748
        }
×
749

750
        return constructor, nil
×
751
}
752

753
// NewImage is the Multipart Image/Meta upload handler.
754
// Request should be of type "multipart/form-data". The parts are
755
// key/value pairs of metadata information except the last one,
756
// which must contain the artifact file.
757
func (d *DeploymentsApiHandlers) NewImage(c *gin.Context) {
3✔
758
        d.newImageWithContext(c.Request.Context(), c)
3✔
759
}
3✔
760

761
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(c *gin.Context) {
3✔
762

3✔
763
        tenantID := c.Param("tenant")
3✔
764

3✔
765
        if tenantID == "" {
3✔
766
                rest.RenderError(
×
767
                        c,
×
768
                        http.StatusBadRequest,
×
769
                        fmt.Errorf("missing tenant id in path"),
×
770
                )
×
771
                return
×
772
        }
×
773

774
        var ctx context.Context
3✔
775
        if tenantID != "default" {
5✔
776
                ident := &identity.Identity{Tenant: tenantID}
2✔
777
                ctx = identity.WithContext(c.Request.Context(), ident)
2✔
778
        } else {
4✔
779
                ctx = c.Request.Context()
2✔
780
        }
2✔
781

782
        d.newImageWithContext(ctx, c)
3✔
783
}
784

785
func (d *DeploymentsApiHandlers) newImageWithContext(
786
        ctx context.Context,
787
        c *gin.Context,
788
) {
3✔
789

3✔
790
        formReader, err := c.Request.MultipartReader()
3✔
791
        if err != nil {
5✔
792
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
793
                return
2✔
794
        }
2✔
795

796
        // parse multipart message
797
        multipartUploadMsg, err := d.ParseMultipart(formReader)
3✔
798

3✔
799
        if err != nil {
5✔
800
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
801
                return
2✔
802
        }
2✔
803

804
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
3✔
805
        if err == nil {
6✔
806
                d.view.RenderSuccessPost(c, imgID)
3✔
807
                return
3✔
808
        }
3✔
809
        var cErr *model.ConflictError
2✔
810
        if errors.As(err, &cErr) {
3✔
811
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
1✔
812
                c.JSON(http.StatusConflict, cErr)
1✔
813
                return
1✔
814
        }
1✔
815
        cause := errors.Cause(err)
1✔
816
        switch cause {
1✔
817
        default:
×
818
                d.view.RenderInternalError(c, err)
×
819
                return
×
820
        case app.ErrModelArtifactNotUnique:
×
821
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
×
822
                return
×
823
        case app.ErrModelParsingArtifactFailed:
1✔
824
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
825
                return
1✔
826
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
827
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
×
828
                return
×
829
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
830
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
831
                io.ErrUnexpectedEOF:
×
832
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
833
                return
×
834
        }
835
}
836

837
func formatArtifactUploadError(err error) error {
2✔
838
        // remove generic message
2✔
839
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
840

2✔
841
        // handle specific cases
2✔
842

2✔
843
        if strings.Contains(errMsg, "invalid checksum") {
2✔
844
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
845
        }
×
846

847
        if strings.Contains(errMsg, "unsupported version") {
2✔
848
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
849
                        "; supported versions are: 1, 2")
×
850
        }
×
851

852
        return errors.New(errMsg)
2✔
853
}
854

855
// GenerateImage s the multipart Raw Data/Meta upload handler.
856
// Request should be of type "multipart/form-data". The parts are
857
// key/valyue pairs of metadata information except the last one,
858
// which must contain the file containing the raw data to be processed
859
// into an artifact.
860
func (d *DeploymentsApiHandlers) GenerateImage(c *gin.Context) {
3✔
861

3✔
862
        formReader, err := c.Request.MultipartReader()
3✔
863
        if err != nil {
4✔
864
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
865
                return
1✔
866
        }
1✔
867

868
        // parse multipart message
869
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
3✔
870
        if err != nil {
4✔
871
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
872
                return
1✔
873
        }
1✔
874

875
        tokenFields := strings.Fields(c.Request.Header.Get("Authorization"))
3✔
876
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
6✔
877
                multipartMsg.Token = tokenFields[1]
3✔
878
        }
3✔
879

880
        imgID, err := d.app.GenerateImage(c.Request.Context(), multipartMsg)
3✔
881
        cause := errors.Cause(err)
3✔
882
        switch cause {
3✔
883
        default:
1✔
884
                d.view.RenderInternalError(c, err)
1✔
885
        case nil:
3✔
886
                d.view.RenderSuccessPost(c, imgID)
3✔
887
        case app.ErrModelArtifactNotUnique:
1✔
888
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
1✔
889
        case app.ErrModelParsingArtifactFailed:
1✔
890
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
891
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
892
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
1✔
893
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
894
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
895
                io.ErrUnexpectedEOF:
×
896
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
897
        }
898
}
899

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

932
                case "size":
3✔
933
                        // Add size limit to the metadata
3✔
934
                        reader := utils.ReadAtMost(part, 20)
3✔
935
                        sz, err := io.ReadAll(reader)
3✔
936
                        if err != nil {
4✔
937
                                return nil, errors.Wrap(err,
1✔
938
                                        "failed to read form value 'size'",
1✔
939
                                )
1✔
940
                        }
1✔
941
                        size, err = strconv.ParseInt(string(sz), 10, 64)
3✔
942
                        if err != nil {
3✔
943
                                return nil, err
×
944
                        }
×
945
                        if size > d.config.MaxImageSize {
3✔
946
                                return nil, ErrModelArtifactFileTooLarge
×
947
                        }
×
948

949
                case "artifact_id":
3✔
950
                        // Add artifact id to the metadata (must be a valid UUID).
3✔
951
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
952
                        b, err := io.ReadAll(reader)
3✔
953
                        if err != nil {
3✔
954
                                return nil, errors.Wrap(err,
×
955
                                        "failed to read form value 'artifact_id'",
×
956
                                )
×
957
                        }
×
958
                        id := string(b)
3✔
959
                        if !govalidator.IsUUID(id) {
5✔
960
                                return nil, errors.New(
2✔
961
                                        "artifact_id is not a valid UUID",
2✔
962
                                )
2✔
963
                        }
2✔
964
                        uploadMsg.ArtifactID = id
2✔
965

966
                case "artifact":
3✔
967
                        // Assign the form-data payload to the artifact reader
3✔
968
                        // and return. The content is consumed elsewhere.
3✔
969
                        if size > 0 {
6✔
970
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
3✔
971
                        } else {
4✔
972
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
1✔
973
                                        part,
1✔
974
                                        d.config.MaxImageSize,
1✔
975
                                )
1✔
976
                        }
1✔
977
                        return uploadMsg, nil
3✔
978

979
                default:
2✔
980
                        // Ignore all non-API sections.
2✔
981
                        continue
2✔
982
                }
983
        }
984
}
985

986
// ParseGenerateImageMultipart parses multipart/form-data message.
987
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
988
        r *multipart.Reader,
989
) (*model.MultipartGenerateImageMsg, error) {
3✔
990
        msg := &model.MultipartGenerateImageMsg{}
3✔
991
        var size int64
3✔
992

3✔
993
ParseLoop:
3✔
994
        for {
6✔
995
                part, err := r.NextPart()
3✔
996
                if err != nil {
4✔
997
                        if err == io.EOF {
2✔
998
                                break
1✔
999
                        }
1000
                        return nil, err
×
1001
                }
1002
                switch strings.ToLower(part.FormName()) {
3✔
1003
                case "args":
3✔
1004
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1005
                        b, err := io.ReadAll(reader)
3✔
1006
                        if err != nil {
3✔
1007
                                return nil, errors.Wrap(err,
×
1008
                                        "failed to read form value 'args'",
×
1009
                                )
×
1010
                        }
×
1011
                        msg.Args = string(b)
3✔
1012

1013
                case "description":
3✔
1014
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1015
                        b, err := io.ReadAll(reader)
3✔
1016
                        if err != nil {
3✔
1017
                                return nil, errors.Wrap(err,
×
1018
                                        "failed to read form value 'description'",
×
1019
                                )
×
1020
                        }
×
1021
                        msg.Description = string(b)
3✔
1022

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

1033
                case "file":
3✔
1034
                        if size > 0 {
4✔
1035
                                msg.FileReader = utils.ReadExactly(part, size)
1✔
1036
                        } else {
4✔
1037
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxGenerateDataSize)
3✔
1038
                        }
3✔
1039
                        break ParseLoop
3✔
1040

1041
                case "name":
3✔
1042
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
1043
                        b, err := io.ReadAll(reader)
3✔
1044
                        if err != nil {
3✔
1045
                                return nil, errors.Wrap(err,
×
1046
                                        "failed to read form value 'name'",
×
1047
                                )
×
1048
                        }
×
1049
                        msg.Name = string(b)
3✔
1050

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

1061
                case "size":
1✔
1062
                        // Add size limit to the metadata
1✔
1063
                        reader := utils.ReadAtMost(part, 20)
1✔
1064
                        sz, err := io.ReadAll(reader)
1✔
1065
                        if err != nil {
2✔
1066
                                return nil, errors.Wrap(err,
1✔
1067
                                        "failed to read form value 'size'",
1✔
1068
                                )
1✔
1069
                        }
1✔
1070
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
1071
                        if err != nil {
1✔
1072
                                return nil, err
×
1073
                        }
×
1074
                        if size > d.config.MaxGenerateDataSize {
1✔
1075
                                return nil, ErrModelArtifactFileTooLarge
×
1076
                        }
×
1077

1078
                default:
×
1079
                        // Ignore non-API sections.
×
1080
                        continue
×
1081
                }
1082
        }
1083

1084
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
3✔
1085
}
1086

1087
// deployments
1088
func (d *DeploymentsApiHandlers) createDeployment(
1089
        c *gin.Context,
1090
        ctx context.Context,
1091
        group string,
1092
) {
3✔
1093
        constructor, err := d.getDeploymentConstructorFromBody(c, group)
3✔
1094
        if err != nil {
6✔
1095
                d.view.RenderError(
3✔
1096
                        c,
3✔
1097
                        errors.Wrap(err, "Validating request body"),
3✔
1098
                        http.StatusBadRequest,
3✔
1099
                )
3✔
1100
                return
3✔
1101
        }
3✔
1102

1103
        id, err := d.app.CreateDeployment(ctx, constructor)
3✔
1104
        switch err {
3✔
1105
        case nil:
3✔
1106
                location := fmt.Sprintf("%s/%s", ApiUrlManagement+ApiUrlManagementDeployments, id)
3✔
1107
                c.Writer.Header().Add("Location", location)
3✔
1108
                c.Status(http.StatusCreated)
3✔
1109
        case app.ErrNoArtifact:
1✔
1110
                d.view.RenderError(c, err, http.StatusUnprocessableEntity)
1✔
1111
        case app.ErrNoDevices:
1✔
1112
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1113
        case app.ErrConflictingDeployment:
2✔
1114
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1115
        default:
1✔
1116
                d.view.RenderInternalError(c, err)
1✔
1117
        }
1118
}
1119

1120
func (d *DeploymentsApiHandlers) PostDeployment(c *gin.Context) {
3✔
1121
        ctx := c.Request.Context()
3✔
1122

3✔
1123
        d.createDeployment(c, ctx, "")
3✔
1124
}
3✔
1125

1126
func (d *DeploymentsApiHandlers) DeployToGroup(c *gin.Context) {
2✔
1127
        ctx := c.Request.Context()
2✔
1128

2✔
1129
        group := c.Param("name")
2✔
1130
        if len(group) < 1 {
2✔
1131
                d.view.RenderError(c, ErrMissingGroupName, http.StatusBadRequest)
×
1132
        }
×
1133
        d.createDeployment(c, ctx, group)
2✔
1134
}
1135

1136
// parseDeviceConfigurationDeploymentPathParams parses expected params
1137
// and check if the params are not empty
1138
func parseDeviceConfigurationDeploymentPathParams(c *gin.Context) (string, string, string, error) {
3✔
1139
        tenantID := c.Param("tenant")
3✔
1140
        deviceID := c.Param(ParamDeviceID)
3✔
1141
        if deviceID == "" {
3✔
1142
                return "", "", "", errors.New("device ID missing")
×
1143
        }
×
1144
        deploymentID := c.Param(ParamDeploymentID)
3✔
1145
        if deploymentID == "" {
3✔
1146
                return "", "", "", errors.New("deployment ID missing")
×
1147
        }
×
1148
        return tenantID, deviceID, deploymentID, nil
3✔
1149
}
1150

1151
// getConfigurationDeploymentConstructorFromBody extracts configuration
1152
// deployment constructor from the request body and validates it
1153
func getConfigurationDeploymentConstructorFromBody(c *gin.Context) (
1154
        *model.ConfigurationDeploymentConstructor, error) {
3✔
1155

3✔
1156
        var constructor *model.ConfigurationDeploymentConstructor
3✔
1157

3✔
1158
        if err := c.ShouldBindJSON(&constructor); err != nil {
5✔
1159
                return nil, err
2✔
1160
        }
2✔
1161

1162
        if err := constructor.Validate(); err != nil {
4✔
1163
                return nil, err
2✔
1164
        }
2✔
1165

1166
        return constructor, nil
2✔
1167
}
1168

1169
// device configuration deployment handler
1170
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1171
        c *gin.Context,
1172
) {
3✔
1173

3✔
1174
        // get path params
3✔
1175
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(c)
3✔
1176
        if err != nil {
3✔
1177
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1178
                return
×
1179
        }
×
1180

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

3✔
1184
        constructor, err := getConfigurationDeploymentConstructorFromBody(c)
3✔
1185
        if err != nil {
6✔
1186
                d.view.RenderError(
3✔
1187
                        c,
3✔
1188
                        errors.Wrap(err, "Validating request body"),
3✔
1189
                        http.StatusBadRequest,
3✔
1190
                )
3✔
1191
                return
3✔
1192
        }
3✔
1193

1194
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
2✔
1195
        switch err {
2✔
1196
        default:
1✔
1197
                d.view.RenderInternalError(c, err)
1✔
1198
        case nil:
2✔
1199
                c.Request.URL.Path = "./deployments"
2✔
1200
                d.view.RenderSuccessPost(c, id)
2✔
1201
        case app.ErrDuplicateDeployment:
2✔
1202
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1203
        case app.ErrInvalidDeploymentID:
1✔
1204
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1205
        }
1206
}
1207

1208
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1209
        c *gin.Context,
1210
        group string,
1211
) (*model.DeploymentConstructor, error) {
3✔
1212
        var constructor *model.DeploymentConstructor
3✔
1213
        if err := c.ShouldBindJSON(&constructor); err != nil {
4✔
1214
                return nil, err
1✔
1215
        }
1✔
1216

1217
        constructor.Group = group
3✔
1218

3✔
1219
        if err := constructor.ValidateNew(); err != nil {
6✔
1220
                return nil, err
3✔
1221
        }
3✔
1222

1223
        return constructor, nil
3✔
1224
}
1225

1226
func (d *DeploymentsApiHandlers) GetDeployment(c *gin.Context) {
2✔
1227
        ctx := c.Request.Context()
2✔
1228

2✔
1229
        id := c.Param("id")
2✔
1230

2✔
1231
        if !govalidator.IsUUID(id) {
3✔
1232
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
1233
                return
1✔
1234
        }
1✔
1235

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

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

1247
        d.view.RenderSuccessGet(c, deployment)
2✔
1248
}
1249

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

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

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

1260
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1261
        if err != nil {
1✔
1262
                d.view.RenderInternalError(c, err)
×
1263
                return
×
1264
        }
×
1265

1266
        if stats == nil {
1✔
1267
                d.view.RenderErrorNotFound(c)
×
1268
                return
×
1269
        }
×
1270

1271
        d.view.RenderSuccessGet(c, stats)
1✔
1272
}
1273

1274
func (d *DeploymentsApiHandlers) GetDeploymentsStats(c *gin.Context) {
1✔
1275

1✔
1276
        ctx := c.Request.Context()
1✔
1277

1✔
1278
        ids := model.DeploymentIDs{}
1✔
1279
        if err := c.ShouldBindJSON(&ids); err != nil {
1✔
1280
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1281
                return
×
1282
        }
×
1283

1284
        if len(ids.IDs) == 0 {
1✔
1285
                c.JSON(http.StatusOK, struct{}{})
×
1286
                return
×
1287
        }
×
1288

1289
        if err := ids.Validate(); err != nil {
2✔
1290
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1291
                return
1✔
1292
        }
1✔
1293

1294
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
1✔
1295
        if err != nil {
2✔
1296
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
2✔
1297
                        d.view.RenderError(c, err, http.StatusNotFound)
1✔
1298
                        return
1✔
1299
                }
1✔
1300
                d.view.RenderInternalError(c, err)
1✔
1301
                return
1✔
1302
        }
1303

1304
        c.JSON(http.StatusOK, stats)
1✔
1305
}
1306

1307
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(c *gin.Context) {
×
1308
        ctx := c.Request.Context()
×
1309

×
1310
        id := c.Param("id")
×
1311

×
1312
        if !govalidator.IsUUID(id) {
×
1313
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1314
                return
×
1315
        }
×
1316

1317
        deployment, err := d.app.GetDeployment(ctx, id)
×
1318
        if err != nil {
×
1319
                d.view.RenderInternalError(c, err)
×
1320
                return
×
1321
        }
×
1322

1323
        if deployment == nil {
×
1324
                d.view.RenderErrorNotFound(c)
×
1325
                return
×
1326
        }
×
1327

1328
        d.view.RenderSuccessGet(c, deployment.DeviceList)
×
1329
}
1330

1331
func (d *DeploymentsApiHandlers) AbortDeployment(c *gin.Context) {
1✔
1332
        ctx := c.Request.Context()
1✔
1333

1✔
1334
        id := c.Param("id")
1✔
1335

1✔
1336
        if !govalidator.IsUUID(id) {
1✔
1337
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1338
                return
×
1339
        }
×
1340

1341
        // receive request body
1342
        var status struct {
1✔
1343
                Status model.DeviceDeploymentStatus
1✔
1344
        }
1✔
1345

1✔
1346
        err := c.ShouldBindJSON(&status)
1✔
1347
        if err != nil {
1✔
1348
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1349
                return
×
1350
        }
×
1351
        // "aborted" is the only supported status
1352
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
1353
                d.view.RenderError(c, ErrUnexpectedDeploymentStatus, http.StatusBadRequest)
×
1354
        }
×
1355

1356
        l := log.FromContext(ctx)
1✔
1357
        l.Infof("Abort deployment: %s", id)
1✔
1358

1✔
1359
        // Check if deployment is finished
1✔
1360
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1361
        if err != nil {
1✔
1362
                d.view.RenderInternalError(c, err)
×
1363
                return
×
1364
        }
×
1365
        if isDeploymentFinished {
2✔
1366
                d.view.RenderError(c, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity)
1✔
1367
                return
1✔
1368
        }
1✔
1369

1370
        // Abort deployments for devices and update deployment stats
1371
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
1372
                d.view.RenderInternalError(c, err)
×
1373
        }
×
1374

1375
        d.view.RenderEmptySuccessResponse(c)
1✔
1376
}
1377

1378
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(c *gin.Context) {
3✔
1379
        var (
3✔
1380
                installed *model.InstalledDeviceDeployment
3✔
1381
                ctx       = c.Request.Context()
3✔
1382
                idata     = identity.FromContext(ctx)
3✔
1383
        )
3✔
1384
        if idata == nil {
4✔
1385
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
1✔
1386
                return
1✔
1387
        }
1✔
1388

1389
        q := c.Request.URL.Query()
3✔
1390
        defer func() {
6✔
1391
                var reEncode bool = false
3✔
1392
                if name := q.Get(ParamArtifactName); name != "" {
6✔
1393
                        q.Set(ParamArtifactName, Redacted)
3✔
1394
                        reEncode = true
3✔
1395
                }
3✔
1396
                if typ := q.Get(ParamDeviceType); typ != "" {
6✔
1397
                        q.Set(ParamDeviceType, Redacted)
3✔
1398
                        reEncode = true
3✔
1399
                }
3✔
1400
                if reEncode {
6✔
1401
                        c.Request.URL.RawQuery = q.Encode()
3✔
1402
                }
3✔
1403
        }()
1404
        if strings.EqualFold(c.Request.Method, http.MethodPost) {
5✔
1405
                // POST
2✔
1406
                installed = new(model.InstalledDeviceDeployment)
2✔
1407
                if err := c.ShouldBindJSON(&installed); err != nil {
3✔
1408
                        d.view.RenderError(c,
1✔
1409
                                errors.Wrap(err, "invalid schema"),
1✔
1410
                                http.StatusBadRequest)
1✔
1411
                        return
1✔
1412
                }
1✔
1413
        } else {
3✔
1414
                // GET or HEAD
3✔
1415
                installed = &model.InstalledDeviceDeployment{
3✔
1416
                        ArtifactName: q.Get(ParamArtifactName),
3✔
1417
                        DeviceType:   q.Get(ParamDeviceType),
3✔
1418
                }
3✔
1419
        }
3✔
1420

1421
        if err := installed.Validate(); err != nil {
4✔
1422
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1423
                return
1✔
1424
        }
1✔
1425

1426
        request := &model.DeploymentNextRequest{
3✔
1427
                DeviceProvides: installed,
3✔
1428
        }
3✔
1429

3✔
1430
        d.getDeploymentForDevice(c, idata, request)
3✔
1431
}
1432

1433
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1434
        c *gin.Context,
1435
        idata *identity.Identity,
1436
        request *model.DeploymentNextRequest,
1437
) {
3✔
1438
        ctx := c.Request.Context()
3✔
1439

3✔
1440
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
3✔
1441
        if err != nil {
5✔
1442
                if err == app.ErrConflictingRequestData {
3✔
1443
                        d.view.RenderError(c, err, http.StatusConflict)
1✔
1444
                } else {
2✔
1445
                        d.view.RenderInternalError(c, err)
1✔
1446
                }
1✔
1447
                return
2✔
1448
        }
1449

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

1488
        d.view.RenderSuccessGet(c, deployment)
3✔
1489
}
1490

1491
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1492
        c *gin.Context,
1493
) {
2✔
1494
        ctx := c.Request.Context()
2✔
1495

2✔
1496
        did := c.Param("id")
2✔
1497

2✔
1498
        idata := identity.FromContext(ctx)
2✔
1499
        if idata == nil {
2✔
1500
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
1501
                return
×
1502
        }
×
1503

1504
        // receive request body
1505
        var report model.StatusReport
2✔
1506

2✔
1507
        err := c.ShouldBindJSON(&report)
2✔
1508
        if err != nil {
3✔
1509
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1510
                return
1✔
1511
        }
1✔
1512
        l := log.FromContext(ctx)
2✔
1513
        l.Infof("status: %+v", report)
2✔
1514
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
2✔
1515
                idata.Subject, model.DeviceDeploymentState{
2✔
1516
                        Status:   report.Status,
2✔
1517
                        SubState: report.SubState,
2✔
1518
                }); err != nil {
3✔
1519

1✔
1520
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
1✔
1521
                        d.view.RenderError(c, err, http.StatusConflict)
×
1522
                } else if err == app.ErrStorageNotFound {
2✔
1523
                        d.view.RenderErrorNotFound(c)
1✔
1524
                } else {
1✔
1525
                        d.view.RenderInternalError(c, err)
×
1526
                }
×
1527
                return
1✔
1528
        }
1529

1530
        d.view.RenderEmptySuccessResponse(c)
2✔
1531
}
1532

1533
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1534
        c *gin.Context,
1535
) {
2✔
1536
        ctx := c.Request.Context()
2✔
1537

2✔
1538
        did := c.Param("id")
2✔
1539

2✔
1540
        if !govalidator.IsUUID(did) {
2✔
1541
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1542
                return
×
1543
        }
×
1544

1545
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
2✔
1546
        if err != nil {
2✔
1547
                switch err {
×
1548
                case app.ErrModelDeploymentNotFound:
×
1549
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1550
                        return
×
1551
                default:
×
1552
                        d.view.RenderInternalError(c, err)
×
1553
                        return
×
1554
                }
1555
        }
1556

1557
        d.view.RenderSuccessGet(c, statuses)
2✔
1558
}
1559

1560
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1561
        c *gin.Context,
1562
) {
1✔
1563
        ctx := c.Request.Context()
1✔
1564

1✔
1565
        did := c.Param("id")
1✔
1566

1✔
1567
        if !govalidator.IsUUID(did) {
1✔
1568
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1569
                return
×
1570
        }
×
1571

1572
        page, perPage, err := rest.ParsePagingParameters(c.Request)
1✔
1573
        if err != nil {
1✔
1574
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1575
                return
×
1576
        }
×
1577

1578
        lq := store.ListQuery{
1✔
1579
                Skip:         int((page - 1) * perPage),
1✔
1580
                Limit:        int(perPage),
1✔
1581
                DeploymentID: did,
1✔
1582
        }
1✔
1583
        if status := c.Request.URL.Query().Get("status"); status != "" {
1✔
1584
                lq.Status = &status
×
1585
        }
×
1586
        if err = lq.Validate(); err != nil {
1✔
1587
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1588
                return
×
1589
        }
×
1590

1591
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1592
        if err != nil {
1✔
1593
                switch err {
×
1594
                case app.ErrModelDeploymentNotFound:
×
1595
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1596
                        return
×
1597
                default:
×
1598
                        d.view.RenderInternalError(c, err)
×
1599
                        return
×
1600
                }
1601
        }
1602

1603
        hasNext := totalCount > int(page*perPage)
1✔
1604
        hints := rest.NewPagingHints().
1✔
1605
                SetPage(page).
1✔
1606
                SetPerPage(perPage).
1✔
1607
                SetHasNext(hasNext).
1✔
1608
                SetTotalCount(int64(totalCount))
1✔
1609

1✔
1610
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
1611
        if err != nil {
1✔
1612
                d.view.RenderInternalError(c, err)
×
1613
                return
×
1614
        }
×
1615

1616
        for _, l := range links {
2✔
1617
                c.Writer.Header().Add(hdrLink, l)
1✔
1618
        }
1✔
1619
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1620
        d.view.RenderSuccessGet(c, statuses)
1✔
1621
}
1622

1623
func ParseLookupQuery(vals url.Values) (model.Query, error) {
3✔
1624
        query := model.Query{}
3✔
1625

3✔
1626
        createdBefore := vals.Get("created_before")
3✔
1627
        if createdBefore != "" {
5✔
1628
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
3✔
1629
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1630
                } else {
2✔
1631
                        query.CreatedBefore = &createdBeforeTime
1✔
1632
                }
1✔
1633
        }
1634

1635
        createdAfter := vals.Get("created_after")
3✔
1636
        if createdAfter != "" {
4✔
1637
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
1✔
1638
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1639
                } else {
1✔
1640
                        query.CreatedAfter = &createdAfterTime
1✔
1641
                }
1✔
1642
        }
1643

1644
        switch strings.ToLower(vals.Get("sort")) {
3✔
1645
        case model.SortDirectionAscending:
1✔
1646
                query.Sort = model.SortDirectionAscending
1✔
1647
        case "", model.SortDirectionDescending:
3✔
1648
                query.Sort = model.SortDirectionDescending
3✔
1649
        default:
×
1650
                return query, ErrInvalidSortDirection
×
1651
        }
1652

1653
        status := vals.Get("status")
3✔
1654
        switch status {
3✔
1655
        case "inprogress":
×
1656
                query.Status = model.StatusQueryInProgress
×
1657
        case "finished":
×
1658
                query.Status = model.StatusQueryFinished
×
1659
        case "pending":
×
1660
                query.Status = model.StatusQueryPending
×
1661
        case "aborted":
×
1662
                query.Status = model.StatusQueryAborted
×
1663
        case "":
3✔
1664
                query.Status = model.StatusQueryAny
3✔
1665
        default:
×
1666
                return query, errors.Errorf("unknown status %s", status)
×
1667

1668
        }
1669

1670
        dType := vals.Get("type")
3✔
1671
        if dType == "" {
6✔
1672
                return query, nil
3✔
1673
        }
3✔
1674
        deploymentType := model.DeploymentType(dType)
×
1675
        if deploymentType == model.DeploymentTypeSoftware ||
×
1676
                deploymentType == model.DeploymentTypeConfiguration {
×
1677
                query.Type = deploymentType
×
1678
        } else {
×
1679
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1680
        }
×
1681

1682
        return query, nil
×
1683
}
1684

1685
func ParseDeploymentLookupQueryV1(vals url.Values) (model.Query, error) {
3✔
1686
        query, err := ParseLookupQuery(vals)
3✔
1687
        if err != nil {
4✔
1688
                return query, err
1✔
1689
        }
1✔
1690

1691
        search := vals.Get("search")
3✔
1692
        if search != "" {
3✔
1693
                query.SearchText = search
×
1694
        }
×
1695

1696
        return query, nil
3✔
1697
}
1698

1699
func ParseDeploymentLookupQueryV2(vals url.Values) (model.Query, error) {
2✔
1700
        query, err := ParseLookupQuery(vals)
2✔
1701
        if err != nil {
2✔
1702
                return query, err
×
1703
        }
×
1704

1705
        query.Names = vals["name"]
2✔
1706
        query.IDs = vals["id"]
2✔
1707

2✔
1708
        return query, nil
2✔
1709
}
1710

1711
func parseEpochToTimestamp(epoch string) (time.Time, error) {
2✔
1712
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
3✔
1713
                return time.Time{}, errors.New("invalid timestamp: " + epoch)
1✔
1714
        } else {
2✔
1715
                return time.Unix(epochInt64, 0).UTC(), nil
1✔
1716
        }
1✔
1717
}
1718

1719
func (d *DeploymentsApiHandlers) LookupDeployment(c *gin.Context) {
3✔
1720
        ctx := c.Request.Context()
3✔
1721
        q := c.Request.URL.Query()
3✔
1722
        defer func() {
6✔
1723
                if search := q.Get("search"); search != "" {
3✔
1724
                        q.Set("search", Redacted)
×
1725
                        c.Request.URL.RawQuery = q.Encode()
×
1726
                }
×
1727
        }()
1728

1729
        query, err := ParseDeploymentLookupQueryV1(q)
3✔
1730
        if err != nil {
4✔
1731
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1732
                return
1✔
1733
        }
1✔
1734

1735
        page, perPage, err := rest.ParsePagingParameters(c.Request)
3✔
1736
        if err != nil {
4✔
1737
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1738
                return
1✔
1739
        }
1✔
1740
        query.Skip = int((page - 1) * perPage)
3✔
1741
        query.Limit = int(perPage + 1)
3✔
1742

3✔
1743
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
3✔
1744
        if err != nil {
4✔
1745
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1746
                return
1✔
1747
        }
1✔
1748
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
3✔
1749

3✔
1750
        len := len(deps)
3✔
1751
        hasNext := false
3✔
1752
        if int64(len) > perPage {
3✔
1753
                hasNext = true
×
1754
                len = int(perPage)
×
1755
        }
×
1756

1757
        hints := rest.NewPagingHints().
3✔
1758
                SetPage(page).
3✔
1759
                SetPerPage(perPage).
3✔
1760
                SetHasNext(hasNext).
3✔
1761
                SetTotalCount(int64(totalCount))
3✔
1762

3✔
1763
        links, err := rest.MakePagingHeaders(c.Request, hints)
3✔
1764
        if err != nil {
3✔
1765
                d.view.RenderInternalError(c, err)
×
1766
                return
×
1767
        }
×
1768
        for _, l := range links {
6✔
1769
                c.Writer.Header().Add(hdrLink, l)
3✔
1770
        }
3✔
1771

1772
        d.view.RenderSuccessGet(c, deps[:len])
3✔
1773
}
1774

1775
func (d *DeploymentsApiHandlers) LookupDeploymentV2(c *gin.Context) {
2✔
1776
        ctx := c.Request.Context()
2✔
1777
        q := c.Request.URL.Query()
2✔
1778
        defer func() {
4✔
1779
                if q.Has("name") {
3✔
1780
                        q["name"] = []string{Redacted}
1✔
1781
                        c.Request.URL.RawQuery = q.Encode()
1✔
1782
                }
1✔
1783
        }()
1784

1785
        query, err := ParseDeploymentLookupQueryV2(q)
2✔
1786
        if err != nil {
2✔
1787
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1788
                return
×
1789
        }
×
1790

1791
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1792
        if err != nil {
3✔
1793
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1794
                return
1✔
1795
        }
1✔
1796
        query.Skip = int((page - 1) * perPage)
2✔
1797
        query.Limit = int(perPage + 1)
2✔
1798

2✔
1799
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
2✔
1800
        if err != nil {
2✔
1801
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1802
                return
×
1803
        }
×
1804
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
2✔
1805

2✔
1806
        len := len(deps)
2✔
1807
        hasNext := false
2✔
1808
        if int64(len) > perPage {
3✔
1809
                hasNext = true
1✔
1810
                len = int(perPage)
1✔
1811
        }
1✔
1812

1813
        hints := rest.NewPagingHints().
2✔
1814
                SetPage(page).
2✔
1815
                SetPerPage(perPage).
2✔
1816
                SetHasNext(hasNext).
2✔
1817
                SetTotalCount(int64(totalCount))
2✔
1818

2✔
1819
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
1820
        if err != nil {
2✔
1821
                d.view.RenderInternalError(c, err)
×
1822
                return
×
1823
        }
×
1824
        for _, l := range links {
4✔
1825
                c.Writer.Header().Add(hdrLink, l)
2✔
1826
        }
2✔
1827

1828
        d.view.RenderSuccessGet(c, deps[:len])
2✔
1829
}
1830

1831
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(c *gin.Context) {
1✔
1832
        ctx := c.Request.Context()
1✔
1833

1✔
1834
        did := c.Param("id")
1✔
1835

1✔
1836
        idata := identity.FromContext(ctx)
1✔
1837
        if idata == nil {
1✔
1838
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
1839
                return
×
1840
        }
×
1841

1842
        // reuse DeploymentLog, device and deployment IDs are ignored when
1843
        // (un-)marshaling DeploymentLog to/from JSON
1844
        var log model.DeploymentLog
1✔
1845

1✔
1846
        err := c.ShouldBindJSON(&log)
1✔
1847
        if err != nil {
1✔
1848
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1849
                return
×
1850
        }
×
1851

1852
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1853
                did, log.Messages); err != nil {
1✔
1854

×
1855
                if err == app.ErrModelDeploymentNotFound {
×
1856
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1857
                } else {
×
1858
                        d.view.RenderInternalError(c, err)
×
1859
                }
×
1860
                return
×
1861
        }
1862

1863
        d.view.RenderEmptySuccessResponse(c)
1✔
1864
}
1865

1866
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(c *gin.Context) {
1✔
1867
        ctx := c.Request.Context()
1✔
1868

1✔
1869
        did := c.Param("id")
1✔
1870
        devid := c.Param("devid")
1✔
1871

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

1✔
1874
        if err != nil {
1✔
1875
                d.view.RenderInternalError(c, err)
×
1876
                return
×
1877
        }
×
1878

1879
        if depl == nil {
1✔
1880
                d.view.RenderErrorNotFound(c)
×
1881
                return
×
1882
        }
×
1883

1884
        d.view.RenderDeploymentLog(c, *depl)
1✔
1885
}
1886

1887
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(c *gin.Context) {
1✔
1888
        ctx := c.Request.Context()
1✔
1889

1✔
1890
        id := c.Param("id")
1✔
1891
        err := d.app.AbortDeviceDeployments(ctx, id)
1✔
1892

1✔
1893
        switch err {
1✔
1894
        case nil, app.ErrStorageNotFound:
1✔
1895
                d.view.RenderEmptySuccessResponse(c)
1✔
1896
        default:
1✔
1897
                d.view.RenderInternalError(c, err)
1✔
1898
        }
1899
}
1900

1901
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(c *gin.Context) {
1✔
1902
        ctx := c.Request.Context()
1✔
1903

1✔
1904
        id := c.Param("id")
1✔
1905
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
1✔
1906

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

1915
func (d *DeploymentsApiHandlers) ListDeviceDeployments(c *gin.Context) {
2✔
1916
        ctx := c.Request.Context()
2✔
1917
        d.listDeviceDeployments(ctx, c, true)
2✔
1918
}
2✔
1919

1920
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(c *gin.Context) {
2✔
1921
        ctx := c.Request.Context()
2✔
1922
        tenantID := c.Param("tenant")
2✔
1923
        if tenantID != "" {
4✔
1924
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
1925
                        Tenant:   tenantID,
2✔
1926
                        IsDevice: true,
2✔
1927
                })
2✔
1928
        }
2✔
1929
        d.listDeviceDeployments(ctx, c, true)
2✔
1930
}
1931

1932
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(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, false)
2✔
1942
}
1943

1944
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1945
        c *gin.Context, byDeviceID bool) {
2✔
1946

2✔
1947
        did := ""
2✔
1948
        var IDs []string
2✔
1949
        if byDeviceID {
4✔
1950
                did = c.Param("id")
2✔
1951
        } else {
4✔
1952
                values := c.Request.URL.Query()
2✔
1953
                if values.Has("id") && len(values["id"]) > 0 {
3✔
1954
                        IDs = values["id"]
1✔
1955
                } else {
3✔
1956
                        d.view.RenderError(c, ErrEmptyID, http.StatusBadRequest)
2✔
1957
                        return
2✔
1958
                }
2✔
1959
        }
1960

1961
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1962
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
3✔
1963
                err = rest.ErrQueryParmLimit(ParamPerPage)
1✔
1964
        }
1✔
1965
        if err != nil {
3✔
1966
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1967
                return
1✔
1968
        }
1✔
1969

1970
        lq := store.ListQueryDeviceDeployments{
2✔
1971
                Skip:     int((page - 1) * perPage),
2✔
1972
                Limit:    int(perPage),
2✔
1973
                DeviceID: did,
2✔
1974
                IDs:      IDs,
2✔
1975
        }
2✔
1976
        if status := c.Request.URL.Query().Get("status"); status != "" {
3✔
1977
                lq.Status = &status
1✔
1978
        }
1✔
1979
        if err = lq.Validate(); err != nil {
3✔
1980
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1981
                return
1✔
1982
        }
1✔
1983

1984
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
2✔
1985
        if err != nil {
3✔
1986
                d.view.RenderInternalError(c, err)
1✔
1987
                return
1✔
1988
        }
1✔
1989
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
2✔
1990

2✔
1991
        hasNext := totalCount > lq.Skip+len(deps)
2✔
1992

2✔
1993
        hints := rest.NewPagingHints().
2✔
1994
                SetPage(page).
2✔
1995
                SetPerPage(perPage).
2✔
1996
                SetHasNext(hasNext).
2✔
1997
                SetTotalCount(int64(totalCount))
2✔
1998

2✔
1999
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
2000
        if err != nil {
2✔
2001
                rest.RenderInternalError(c, err)
×
2002
                return
×
2003
        }
×
2004
        for _, l := range links {
4✔
2005
                c.Writer.Header().Add(hdrLink, l)
2✔
2006
        }
2✔
2007

2008
        d.view.RenderSuccessGet(c, deps)
2✔
2009
}
2010

2011
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(c *gin.Context) {
1✔
2012
        ctx := c.Request.Context()
1✔
2013
        tenantID := c.Param("tenantID")
1✔
2014
        if tenantID != "" {
1✔
2015
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
×
2016
                        Tenant:   tenantID,
×
2017
                        IsDevice: true,
×
2018
                })
×
2019
        }
×
2020

2021
        id := c.Param("id")
1✔
2022

1✔
2023
        // Decommission deployments for devices and update deployment stats
1✔
2024
        err := d.app.DecommissionDevice(ctx, id)
1✔
2025

1✔
2026
        switch err {
1✔
2027
        case nil, app.ErrStorageNotFound:
1✔
2028
                d.view.RenderEmptySuccessResponse(c)
1✔
2029
        default:
×
2030
                d.view.RenderInternalError(c, err)
×
2031

2032
        }
2033
}
2034

2035
// tenants
2036

2037
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(c *gin.Context) {
2✔
2038
        ctx := c.Request.Context()
2✔
2039

2✔
2040
        defer c.Request.Body.Close()
2✔
2041

2✔
2042
        tenant, err := model.ParseNewTenantReq(c.Request.Body)
2✔
2043
        if err != nil {
4✔
2044
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
2045
                return
2✔
2046
        }
2✔
2047

2048
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
2049
        if err != nil {
1✔
2050
                d.view.RenderInternalError(c, err)
×
2051
                return
×
2052
        }
×
2053

2054
        c.Status(http.StatusCreated)
1✔
2055
}
2056

2057
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
2058
        c *gin.Context,
2059
) {
2✔
2060
        tenantID := c.Param("tenant")
2✔
2061
        if tenantID == "" {
3✔
2062

1✔
2063
                d.view.RenderError(c, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
2064
                return
1✔
2065
        }
1✔
2066
        c.Request = c.Request.WithContext(identity.WithContext(
2✔
2067
                c.Request.Context(),
2✔
2068
                &identity.Identity{Tenant: tenantID},
2✔
2069
        ))
2✔
2070
        d.LookupDeployment(c)
2✔
2071
}
2072

2073
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
2074
        c *gin.Context,
2075
) {
3✔
2076

3✔
2077
        tenantID := c.Param("tenant")
3✔
2078

3✔
2079
        ctx := identity.WithContext(
3✔
2080
                c.Request.Context(),
3✔
2081
                &identity.Identity{Tenant: tenantID},
3✔
2082
        )
3✔
2083

3✔
2084
        settings, err := d.app.GetStorageSettings(ctx)
3✔
2085
        if err != nil {
4✔
2086
                d.view.RenderInternalError(c, err)
1✔
2087
                return
1✔
2088
        }
1✔
2089

2090
        d.view.RenderSuccessGet(c, settings)
3✔
2091
}
2092

2093
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
2094
        c *gin.Context,
2095
) {
3✔
2096

3✔
2097
        defer c.Request.Body.Close()
3✔
2098

3✔
2099
        tenantID := c.Param("tenant")
3✔
2100

3✔
2101
        ctx := identity.WithContext(
3✔
2102
                c.Request.Context(),
3✔
2103
                &identity.Identity{Tenant: tenantID},
3✔
2104
        )
3✔
2105

3✔
2106
        settings, err := model.ParseStorageSettingsRequest(c.Request.Body)
3✔
2107
        if err != nil {
6✔
2108
                d.view.RenderError(c, err, http.StatusBadRequest)
3✔
2109
                return
3✔
2110
        }
3✔
2111

2112
        err = d.app.SetStorageSettings(ctx, settings)
2✔
2113
        if err != nil {
3✔
2114
                d.view.RenderInternalError(c, err)
1✔
2115
                return
1✔
2116
        }
1✔
2117

2118
        c.Status(http.StatusNoContent)
2✔
2119
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc