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

mendersoftware / mender-server / 1943770040

23 Jul 2025 01:08PM UTC coverage: 65.435% (-0.05%) from 65.481%
1943770040

Pull #805

gitlab-ci

alfrunes
test: Fix tests making invalid assertions for request body

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

86 of 94 new or added lines in 3 files covered. (91.49%)

3 existing lines in 2 files now uncovered.

32306 of 49371 relevant lines covered (65.44%)

1.39 hits per line

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

79.15
/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
        hdrLocation      = "Location"
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
        // RequestSize is the maximum request body size
143
        MaxRequestSize int64
144

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

322
        return filter
3✔
323
}
324

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

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

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

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

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

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

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

372
        return filter
1✔
373
}
374

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

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

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

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

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

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

403
// images
404

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

565
const maxMetadataSize = 2048
566

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
753
        var constructor *model.ImageMeta
×
754

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

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

763
        return constructor, nil
×
764
}
765

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

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

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

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

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

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

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

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

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

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

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

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

2✔
854
        // handle specific cases
2✔
855

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1179
        return constructor, nil
2✔
1180
}
1181

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

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

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

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

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

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

1230
        constructor.Group = group
3✔
1231

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

1236
        return constructor, nil
3✔
1237
}
1238

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1681
        }
1682

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

1695
        return query, nil
×
1696
}
1697

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

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

1709
        return query, nil
3✔
1710
}
1711

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1933
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(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, true)
2✔
1943
}
1944

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2045
        }
2046
}
2047

2048
// tenants
2049

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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