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

mendersoftware / mender-server / 1925715791

14 Jul 2025 02:01PM UTC coverage: 65.487% (-0.02%) from 65.504%
1925715791

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

145 of 237 new or added lines in 7 files covered. (61.18%)

129 existing lines in 3 files now uncovered.

32534 of 49680 relevant lines covered (65.49%)

1.38 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
        "github.com/mendersoftware/mender-server/pkg/rest_utils"
39

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

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

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

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

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

86
const Redacted = "REDACTED"
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

310
        return filter
3✔
311
}
312

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

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

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

1✔
319
        var exactNames []string
1✔
320
        var nameprefixes []string
1✔
321

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

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

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

360
        return filter
1✔
361
}
362

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

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

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

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

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

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

391
// images
392

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

553
const maxMetadataSize = 2048
554

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
741
        var constructor *model.ImageMeta
×
742

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

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

751
        return constructor, nil
×
752
}
753

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

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

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

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

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

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

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

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

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

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

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

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

2✔
842
        // handle specific cases
2✔
843

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1167
        return constructor, nil
2✔
1168
}
1169

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

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

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

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

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

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

1218
        constructor.Group = group
3✔
1219

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

1224
        return constructor, nil
3✔
1225
}
1226

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1669
        }
1670

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

1683
        return query, nil
×
1684
}
1685

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

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

1697
        return query, nil
3✔
1698
}
1699

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2033
        }
2034
}
2035

2036
// tenants
2037

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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