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

mendersoftware / mender-server / 1943731786

23 Jul 2025 12:52PM UTC coverage: 65.476% (-0.005%) from 65.481%
1943731786

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

87 of 97 new or added lines in 3 files covered. (89.69%)

1099 existing lines in 18 files now uncovered.

32143 of 49091 relevant lines covered (65.48%)

1.39 hits per line

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

79.27
/backend/services/deployments/api/http/api_deployments.go
1
// Copyright 2024 Northern.tech AS
2
//
3
//        Licensed under the Apache License, Version 2.0 (the "License");
4
//        you may not use this file except in compliance with the License.
5
//        You may obtain a copy of the License at
6
//
7
//            http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//        Unless required by applicable law or agreed to in writing, software
10
//        distributed under the License is distributed on an "AS IS" BASIS,
11
//        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//        See the License for the specific language governing permissions and
13
//        limitations under the License.
14

15
package http
16

17
import (
18
        "context"
19
        "encoding/json"
20
        "fmt"
21
        "io"
22
        "mime/multipart"
23
        "net/http"
24
        "net/url"
25
        "strconv"
26
        "strings"
27
        "time"
28

29
        "github.com/asaskevich/govalidator"
30
        "github.com/gin-gonic/gin"
31
        "github.com/pkg/errors"
32

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

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

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

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

60
const (
61
        // Header Constants
62
        hdrTotalCount    = "X-Total-Count"
63
        hdrLink          = "Link"
64
        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
type limitResponse struct {
326
        Limit uint64 `json:"limit"`
327
        Usage uint64 `json:"usage"`
328
}
329

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

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

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

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

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

353
// images
354

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

471
const maxMetadataSize = 2048
472

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
UNCOV
622
        id := c.Param("id")
×
UNCOV
623

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

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

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

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

UNCOV
654
        d.view.RenderSuccessPut(c)
×
655
}
656

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

×
UNCOV
659
        var constructor *model.ImageMeta
×
UNCOV
660

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

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

UNCOV
669
        return constructor, nil
×
670
}
671

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

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

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

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

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

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

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

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

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

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

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

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

2✔
760
        // handle specific cases
2✔
761

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1085
        return constructor, nil
2✔
1086
}
1087

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

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

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

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

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

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

1136
        constructor.Group = group
3✔
1137

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

1142
        return constructor, nil
3✔
1143
}
1144

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
UNCOV
1229
        id := c.Param("id")
×
UNCOV
1230

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1587
        }
1588

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

1601
        return query, nil
×
1602
}
1603

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

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

1615
        return query, nil
3✔
1616
}
1617

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1951
        }
1952
}
1953

1954
// tenants
1955

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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