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

mendersoftware / mender-server / 1931953235

17 Jul 2025 09:41AM UTC coverage: 65.428% (-0.09%) from 65.521%
1931953235

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

81 of 90 new or added lines in 2 files covered. (90.0%)

345 existing lines in 14 files now uncovered.

32050 of 48985 relevant lines covered (65.43%)

1.39 hits per line

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

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

15
package http
16

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

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

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

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

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

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

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

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

85
const Redacted = "REDACTED"
86

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

309
        return filter
3✔
310
}
311

312
type limitResponse struct {
313
        Limit uint64 `json:"limit"`
314
        Usage uint64 `json:"usage"`
315
}
316

317
func (d *DeploymentsApiHandlers) GetLimit(c *gin.Context) {
1✔
318

1✔
319
        name := c.Param("name")
1✔
320

1✔
321
        if !model.IsValidLimit(name) {
2✔
322
                d.view.RenderError(c,
1✔
323
                        errors.Errorf("unsupported limit %s", name),
1✔
324
                        http.StatusBadRequest)
1✔
325
                return
1✔
326
        }
1✔
327

328
        limit, err := d.app.GetLimit(c.Request.Context(), name)
1✔
329
        if err != nil {
2✔
330
                d.view.RenderInternalError(c, err)
1✔
331
                return
1✔
332
        }
1✔
333

334
        d.view.RenderSuccessGet(c, limitResponse{
1✔
335
                Limit: limit.Value,
1✔
336
                Usage: 0, // TODO fill this when ready
1✔
337
        })
1✔
338
}
339

340
// images
341

342
func (d *DeploymentsApiHandlers) GetImage(c *gin.Context) {
2✔
343

2✔
344
        id := c.Param("id")
2✔
345

2✔
346
        if !govalidator.IsUUID(id) {
3✔
347
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
348
                return
1✔
349
        }
1✔
350

351
        image, err := d.app.GetImage(c.Request.Context(), id)
2✔
352
        if err != nil {
2✔
UNCOV
353
                d.view.RenderInternalError(c, err)
×
UNCOV
354
                return
×
UNCOV
355
        }
×
356

357
        if image == nil {
3✔
358
                d.view.RenderErrorNotFound(c)
1✔
359
                return
1✔
360
        }
1✔
361

362
        d.view.RenderSuccessGet(c, image)
2✔
363
}
364

365
func (d *DeploymentsApiHandlers) GetImages(c *gin.Context) {
3✔
366

3✔
367
        defer redactReleaseName(c.Request)
3✔
368
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, false)
3✔
369

3✔
370
        list, _, err := d.app.ListImages(c.Request.Context(), filter)
3✔
371
        if err != nil {
4✔
372
                d.view.RenderInternalError(c, err)
1✔
373
                return
1✔
374
        }
1✔
375

376
        d.view.RenderSuccessGet(c, list)
3✔
377
}
378

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

1✔
381
        defer redactReleaseName(c.Request)
1✔
382
        filter := getReleaseOrImageFilter(c.Request, listReleasesV1, true)
1✔
383

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

390
        hasNext := totalCount > int(filter.Page*filter.PerPage)
1✔
391

1✔
392
        hints := rest.NewPagingHints().
1✔
393
                SetPage(int64(filter.Page)).
1✔
394
                SetPerPage(int64(filter.PerPage)).
1✔
395
                SetHasNext(hasNext).
1✔
396
                SetTotalCount(int64(totalCount))
1✔
397

1✔
398
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
399
        if err != nil {
1✔
UNCOV
400
                d.view.RenderInternalError(c, err)
×
UNCOV
401
                return
×
UNCOV
402
        }
×
403

404
        for _, l := range links {
2✔
405
                c.Writer.Header().Add(hdrLink, l)
1✔
406
        }
1✔
407
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
408

1✔
409
        d.view.RenderSuccessGet(c, list)
1✔
410
}
411

412
func (d *DeploymentsApiHandlers) DownloadLink(c *gin.Context) {
1✔
413

1✔
414
        id := c.Param("id")
1✔
415

1✔
416
        if !govalidator.IsUUID(id) {
1✔
UNCOV
417
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
UNCOV
418
                return
×
UNCOV
419
        }
×
420

421
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
1✔
422
        link, err := d.app.DownloadLink(c.Request.Context(), id,
1✔
423
                time.Duration(expireSeconds)*time.Second)
1✔
424
        if err != nil {
1✔
UNCOV
425
                d.view.RenderInternalError(c, err)
×
UNCOV
426
                return
×
UNCOV
427
        }
×
428

429
        if link == nil {
1✔
430
                d.view.RenderErrorNotFound(c)
×
431
                return
×
UNCOV
432
        }
×
433

434
        d.view.RenderSuccessGet(c, link)
1✔
435
}
436

437
func (d *DeploymentsApiHandlers) UploadLink(c *gin.Context) {
2✔
438

2✔
439
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageUploadExpireSeconds)
2✔
440
        link, err := d.app.UploadLink(
2✔
441
                c.Request.Context(),
2✔
442
                time.Duration(expireSeconds)*time.Second,
2✔
443
                d.config.EnableDirectUploadSkipVerify,
2✔
444
        )
2✔
445
        if err != nil {
3✔
446
                d.view.RenderInternalError(c, err)
1✔
447
                return
1✔
448
        }
1✔
449

450
        if link == nil {
3✔
451
                d.view.RenderErrorNotFound(c)
1✔
452
                return
1✔
453
        }
1✔
454

455
        d.view.RenderSuccessGet(c, link)
2✔
456
}
457

458
const maxMetadataSize = 2048
459

460
func (d *DeploymentsApiHandlers) CompleteUpload(c *gin.Context) {
2✔
461
        ctx := c.Request.Context()
2✔
462
        l := log.FromContext(ctx)
2✔
463

2✔
464
        artifactID := c.Param(ParamID)
2✔
465

2✔
466
        var metadata *model.DirectUploadMetadata
2✔
467
        if d.config.EnableDirectUploadSkipVerify {
3✔
468
                var directMetadata model.DirectUploadMetadata
1✔
469
                bodyBuffer := make([]byte, maxMetadataSize)
1✔
470
                n, err := io.ReadFull(c.Request.Body, bodyBuffer)
1✔
471
                c.Request.Body.Close()
1✔
472
                if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
1✔
UNCOV
473
                        l.Errorf("error reading post body data: %s (read: %d)", err.Error(), n)
×
474
                } else {
1✔
475
                        err = json.Unmarshal(bodyBuffer[:n], &directMetadata)
1✔
476
                        if err == nil {
2✔
477
                                if directMetadata.Validate() == nil {
2✔
478
                                        metadata = &directMetadata
1✔
479
                                }
1✔
480
                        } else {
1✔
481
                                l.Errorf("error parsing json data: %s", err.Error())
1✔
482
                        }
1✔
483
                }
484
        }
485

486
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
2✔
487
        switch errors.Cause(err) {
2✔
488
        case nil:
2✔
489
                // c.Writer.Header().Set("Link", "FEAT: Upload status API")
2✔
490
                c.Status(http.StatusAccepted)
2✔
491
        case app.ErrUploadNotFound:
1✔
492
                d.view.RenderErrorNotFound(c)
1✔
493
        default:
1✔
494
                d.view.RenderInternalError(c, err)
1✔
495
        }
496
}
497

498
func (d *DeploymentsApiHandlers) DownloadConfiguration(c *gin.Context) {
3✔
499
        if d.config.PresignSecret == nil {
4✔
500
                d.view.RenderErrorNotFound(c)
1✔
501
                return
1✔
502
        }
1✔
503
        var (
3✔
504
                deviceID, _     = url.PathUnescape(c.Param(ParamDeviceID))
3✔
505
                deviceType, _   = url.PathUnescape(c.Param(ParamDeviceType))
3✔
506
                deploymentID, _ = url.PathUnescape(c.Param(ParamDeploymentID))
3✔
507
        )
3✔
508
        if deviceID == "" || deviceType == "" || deploymentID == "" {
3✔
UNCOV
509
                d.view.RenderErrorNotFound(c)
×
UNCOV
510
                return
×
UNCOV
511
        }
×
512

513
        var (
3✔
514
                tenantID string
3✔
515
                q        = c.Request.URL.Query()
3✔
516
                err      error
3✔
517
        )
3✔
518
        tenantID = q.Get(ParamTenantID)
3✔
519
        sig := model.NewRequestSignature(c.Request, d.config.PresignSecret)
3✔
520
        if err = sig.Validate(); err != nil {
6✔
521
                switch cause := errors.Cause(err); cause {
3✔
522
                case model.ErrLinkExpired:
1✔
523
                        d.view.RenderError(c, cause, http.StatusForbidden)
1✔
524
                default:
3✔
525
                        d.view.RenderError(c,
3✔
526
                                errors.Wrap(err, "invalid request parameters"),
3✔
527
                                http.StatusBadRequest,
3✔
528
                        )
3✔
529
                }
530
                return
3✔
531
        }
532

533
        if !sig.VerifyHMAC256() {
4✔
534
                d.view.RenderError(c,
2✔
535
                        errors.New("signature invalid"),
2✔
536
                        http.StatusForbidden,
2✔
537
                )
2✔
538
                return
2✔
539
        }
2✔
540

541
        // Validate request signature
542
        ctx := identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
543
                Subject:  deviceID,
2✔
544
                Tenant:   tenantID,
2✔
545
                IsDevice: true,
2✔
546
        })
2✔
547

2✔
548
        artifact, err := d.app.GenerateConfigurationImage(ctx, deviceType, deploymentID)
2✔
549
        if err != nil {
3✔
550
                switch cause := errors.Cause(err); cause {
1✔
551
                case app.ErrModelDeploymentNotFound:
1✔
552
                        d.view.RenderError(c,
1✔
553
                                errors.Errorf(
1✔
554
                                        "deployment with id '%s' not found",
1✔
555
                                        deploymentID,
1✔
556
                                ),
1✔
557
                                http.StatusNotFound,
1✔
558
                        )
1✔
559
                default:
1✔
560
                        d.view.RenderInternalError(c, err)
1✔
561
                }
562
                return
1✔
563
        }
564
        artifactPayload, err := io.ReadAll(artifact)
2✔
565
        if err != nil {
3✔
566
                d.view.RenderInternalError(c, err)
1✔
567
                return
1✔
568
        }
1✔
569

570
        rw := c.Writer
2✔
571
        hdr := rw.Header()
2✔
572
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
2✔
573
        hdr.Set("Content-Type", app.ArtifactContentType)
2✔
574
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
2✔
575
        c.Status(http.StatusOK)
2✔
576
        _, err = rw.Write(artifactPayload)
2✔
577
        if err != nil {
2✔
UNCOV
578
                // There's not anything we can do here in terms of the response.
×
UNCOV
579
                _ = c.Error(err)
×
UNCOV
580
        }
×
581
}
582

583
func (d *DeploymentsApiHandlers) DeleteImage(c *gin.Context) {
1✔
584

1✔
585
        id := c.Param("id")
1✔
586

1✔
587
        if !govalidator.IsUUID(id) {
1✔
UNCOV
588
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
UNCOV
589
                return
×
590
        }
×
591

592
        if err := d.app.DeleteImage(c.Request.Context(), id); err != nil {
2✔
593
                switch err {
1✔
UNCOV
594
                default:
×
UNCOV
595
                        d.view.RenderInternalError(c, err)
×
UNCOV
596
                case app.ErrImageMetaNotFound:
×
UNCOV
597
                        d.view.RenderErrorNotFound(c)
×
598
                case app.ErrModelImageInActiveDeployment:
1✔
599
                        d.view.RenderError(c, ErrArtifactUsedInActiveDeployment, http.StatusConflict)
1✔
600
                }
601
                return
1✔
602
        }
603

604
        d.view.RenderSuccessDelete(c)
1✔
605
}
606

607
func (d *DeploymentsApiHandlers) EditImage(c *gin.Context) {
×
608

×
609
        id := c.Param("id")
×
UNCOV
610

×
UNCOV
611
        if !govalidator.IsUUID(id) {
×
UNCOV
612
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
UNCOV
613
                return
×
UNCOV
614
        }
×
615

UNCOV
616
        constructor, err := getImageMetaFromBody(c)
×
UNCOV
617
        if err != nil {
×
UNCOV
618
                d.view.RenderError(
×
619
                        c,
×
620
                        errors.Wrap(err, "Validating request body"),
×
621
                        http.StatusBadRequest,
×
622
                )
×
623
                return
×
624
        }
×
625

626
        found, err := d.app.EditImage(c.Request.Context(), id, constructor)
×
UNCOV
627
        if err != nil {
×
628
                if err == app.ErrModelImageUsedInAnyDeployment {
×
629
                        d.view.RenderError(c, err, http.StatusUnprocessableEntity)
×
630
                        return
×
631
                }
×
632
                d.view.RenderInternalError(c, err)
×
633
                return
×
634
        }
635

636
        if !found {
×
UNCOV
637
                d.view.RenderErrorNotFound(c)
×
638
                return
×
639
        }
×
640

641
        d.view.RenderSuccessPut(c)
×
642
}
643

644
func getImageMetaFromBody(c *gin.Context) (*model.ImageMeta, error) {
×
645

×
UNCOV
646
        var constructor *model.ImageMeta
×
UNCOV
647

×
648
        if err := c.ShouldBindJSON(&constructor); err != nil {
×
649
                return nil, err
×
650
        }
×
651

UNCOV
652
        if err := constructor.Validate(); err != nil {
×
653
                return nil, err
×
UNCOV
654
        }
×
655

656
        return constructor, nil
×
657
}
658

659
// NewImage is the Multipart Image/Meta upload handler.
660
// Request should be of type "multipart/form-data". The parts are
661
// key/value pairs of metadata information except the last one,
662
// which must contain the artifact file.
663
func (d *DeploymentsApiHandlers) NewImage(c *gin.Context) {
3✔
664
        d.newImageWithContext(c.Request.Context(), c)
3✔
665
}
3✔
666

667
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(c *gin.Context) {
3✔
668

3✔
669
        tenantID := c.Param("tenant")
3✔
670

3✔
671
        if tenantID == "" {
3✔
UNCOV
672
                rest.RenderError(
×
UNCOV
673
                        c,
×
UNCOV
674
                        http.StatusBadRequest,
×
UNCOV
675
                        fmt.Errorf("missing tenant id in path"),
×
UNCOV
676
                )
×
UNCOV
677
                return
×
UNCOV
678
        }
×
679

680
        var ctx context.Context
3✔
681
        if tenantID != "default" {
5✔
682
                ident := &identity.Identity{Tenant: tenantID}
2✔
683
                ctx = identity.WithContext(c.Request.Context(), ident)
2✔
684
        } else {
4✔
685
                ctx = c.Request.Context()
2✔
686
        }
2✔
687

688
        d.newImageWithContext(ctx, c)
3✔
689
}
690

691
func (d *DeploymentsApiHandlers) newImageWithContext(
692
        ctx context.Context,
693
        c *gin.Context,
694
) {
3✔
695

3✔
696
        formReader, err := c.Request.MultipartReader()
3✔
697
        if err != nil {
5✔
698
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
699
                return
2✔
700
        }
2✔
701

702
        // parse multipart message
703
        multipartUploadMsg, err := d.ParseMultipart(formReader)
3✔
704

3✔
705
        if err != nil {
5✔
706
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
707
                return
2✔
708
        }
2✔
709

710
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
3✔
711
        if err == nil {
6✔
712
                d.view.RenderSuccessPost(c, imgID)
3✔
713
                return
3✔
714
        }
3✔
715
        var cErr *model.ConflictError
2✔
716
        if errors.As(err, &cErr) {
3✔
717
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
1✔
718
                c.JSON(http.StatusConflict, cErr)
1✔
719
                return
1✔
720
        }
1✔
721
        cause := errors.Cause(err)
1✔
722
        switch cause {
1✔
UNCOV
723
        default:
×
UNCOV
724
                d.view.RenderInternalError(c, err)
×
UNCOV
725
                return
×
UNCOV
726
        case app.ErrModelArtifactNotUnique:
×
UNCOV
727
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
×
UNCOV
728
                return
×
729
        case app.ErrModelParsingArtifactFailed:
1✔
730
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
731
                return
1✔
UNCOV
732
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
UNCOV
733
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
×
UNCOV
734
                return
×
735
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
736
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
737
                io.ErrUnexpectedEOF:
×
738
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
739
                return
×
740
        }
741
}
742

743
func formatArtifactUploadError(err error) error {
2✔
744
        // remove generic message
2✔
745
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
2✔
746

2✔
747
        // handle specific cases
2✔
748

2✔
749
        if strings.Contains(errMsg, "invalid checksum") {
2✔
750
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
751
        }
×
752

753
        if strings.Contains(errMsg, "unsupported version") {
2✔
UNCOV
754
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
UNCOV
755
                        "; supported versions are: 1, 2")
×
UNCOV
756
        }
×
757

758
        return errors.New(errMsg)
2✔
759
}
760

761
// GenerateImage s the multipart Raw Data/Meta upload handler.
762
// Request should be of type "multipart/form-data". The parts are
763
// key/valyue pairs of metadata information except the last one,
764
// which must contain the file containing the raw data to be processed
765
// into an artifact.
766
func (d *DeploymentsApiHandlers) GenerateImage(c *gin.Context) {
3✔
767

3✔
768
        formReader, err := c.Request.MultipartReader()
3✔
769
        if err != nil {
4✔
770
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
771
                return
1✔
772
        }
1✔
773

774
        // parse multipart message
775
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
3✔
776
        if err != nil {
4✔
777
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
778
                return
1✔
779
        }
1✔
780

781
        tokenFields := strings.Fields(c.Request.Header.Get("Authorization"))
3✔
782
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
6✔
783
                multipartMsg.Token = tokenFields[1]
3✔
784
        }
3✔
785

786
        imgID, err := d.app.GenerateImage(c.Request.Context(), multipartMsg)
3✔
787
        cause := errors.Cause(err)
3✔
788
        switch cause {
3✔
789
        default:
1✔
790
                d.view.RenderInternalError(c, err)
1✔
791
        case nil:
3✔
792
                d.view.RenderSuccessPost(c, imgID)
3✔
793
        case app.ErrModelArtifactNotUnique:
1✔
794
                d.view.RenderError(c, cause, http.StatusUnprocessableEntity)
1✔
795
        case app.ErrModelParsingArtifactFailed:
1✔
796
                d.view.RenderError(c, formatArtifactUploadError(err), http.StatusBadRequest)
1✔
797
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
798
                d.view.RenderError(c, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge)
1✔
799
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
800
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
UNCOV
801
                io.ErrUnexpectedEOF:
×
UNCOV
802
                d.view.RenderError(c, cause, http.StatusBadRequest)
×
803
        }
804
}
805

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

838
                case "size":
3✔
839
                        // Add size limit to the metadata
3✔
840
                        reader := utils.ReadAtMost(part, 20)
3✔
841
                        sz, err := io.ReadAll(reader)
3✔
842
                        if err != nil {
4✔
843
                                return nil, errors.Wrap(err,
1✔
844
                                        "failed to read form value 'size'",
1✔
845
                                )
1✔
846
                        }
1✔
847
                        size, err = strconv.ParseInt(string(sz), 10, 64)
3✔
848
                        if err != nil {
3✔
UNCOV
849
                                return nil, err
×
UNCOV
850
                        }
×
851
                        if size > d.config.MaxImageSize {
3✔
UNCOV
852
                                return nil, ErrModelArtifactFileTooLarge
×
UNCOV
853
                        }
×
854

855
                case "artifact_id":
3✔
856
                        // Add artifact id to the metadata (must be a valid UUID).
3✔
857
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
858
                        b, err := io.ReadAll(reader)
3✔
859
                        if err != nil {
3✔
UNCOV
860
                                return nil, errors.Wrap(err,
×
861
                                        "failed to read form value 'artifact_id'",
×
862
                                )
×
UNCOV
863
                        }
×
864
                        id := string(b)
3✔
865
                        if !govalidator.IsUUID(id) {
5✔
866
                                return nil, errors.New(
2✔
867
                                        "artifact_id is not a valid UUID",
2✔
868
                                )
2✔
869
                        }
2✔
870
                        uploadMsg.ArtifactID = id
2✔
871

872
                case "artifact":
3✔
873
                        // Assign the form-data payload to the artifact reader
3✔
874
                        // and return. The content is consumed elsewhere.
3✔
875
                        if size > 0 {
6✔
876
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
3✔
877
                        } else {
4✔
878
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
1✔
879
                                        part,
1✔
880
                                        d.config.MaxImageSize,
1✔
881
                                )
1✔
882
                        }
1✔
883
                        return uploadMsg, nil
3✔
884

885
                default:
2✔
886
                        // Ignore all non-API sections.
2✔
887
                        continue
2✔
888
                }
889
        }
890
}
891

892
// ParseGenerateImageMultipart parses multipart/form-data message.
893
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
894
        r *multipart.Reader,
895
) (*model.MultipartGenerateImageMsg, error) {
3✔
896
        msg := &model.MultipartGenerateImageMsg{}
3✔
897
        var size int64
3✔
898

3✔
899
ParseLoop:
3✔
900
        for {
6✔
901
                part, err := r.NextPart()
3✔
902
                if err != nil {
4✔
903
                        if err == io.EOF {
2✔
904
                                break
1✔
905
                        }
UNCOV
906
                        return nil, err
×
907
                }
908
                switch strings.ToLower(part.FormName()) {
3✔
909
                case "args":
3✔
910
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
911
                        b, err := io.ReadAll(reader)
3✔
912
                        if err != nil {
3✔
UNCOV
913
                                return nil, errors.Wrap(err,
×
UNCOV
914
                                        "failed to read form value 'args'",
×
UNCOV
915
                                )
×
UNCOV
916
                        }
×
917
                        msg.Args = string(b)
3✔
918

919
                case "description":
3✔
920
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
921
                        b, err := io.ReadAll(reader)
3✔
922
                        if err != nil {
3✔
UNCOV
923
                                return nil, errors.Wrap(err,
×
UNCOV
924
                                        "failed to read form value 'description'",
×
925
                                )
×
926
                        }
×
927
                        msg.Description = string(b)
3✔
928

929
                case "device_types_compatible":
3✔
930
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
931
                        b, err := io.ReadAll(reader)
3✔
932
                        if err != nil {
3✔
UNCOV
933
                                return nil, errors.Wrap(err,
×
UNCOV
934
                                        "failed to read form value 'device_types_compatible'",
×
935
                                )
×
936
                        }
×
937
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
3✔
938

939
                case "file":
3✔
940
                        if size > 0 {
4✔
941
                                msg.FileReader = utils.ReadExactly(part, size)
1✔
942
                        } else {
4✔
943
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxGenerateDataSize)
3✔
944
                        }
3✔
945
                        break ParseLoop
3✔
946

947
                case "name":
3✔
948
                        reader := utils.ReadAtMost(part, MaxFormParamSize)
3✔
949
                        b, err := io.ReadAll(reader)
3✔
950
                        if err != nil {
3✔
UNCOV
951
                                return nil, errors.Wrap(err,
×
UNCOV
952
                                        "failed to read form value 'name'",
×
UNCOV
953
                                )
×
UNCOV
954
                        }
×
955
                        msg.Name = string(b)
3✔
956

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

967
                case "size":
1✔
968
                        // Add size limit to the metadata
1✔
969
                        reader := utils.ReadAtMost(part, 20)
1✔
970
                        sz, err := io.ReadAll(reader)
1✔
971
                        if err != nil {
2✔
972
                                return nil, errors.Wrap(err,
1✔
973
                                        "failed to read form value 'size'",
1✔
974
                                )
1✔
975
                        }
1✔
976
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
977
                        if err != nil {
1✔
UNCOV
978
                                return nil, err
×
UNCOV
979
                        }
×
980
                        if size > d.config.MaxGenerateDataSize {
1✔
UNCOV
981
                                return nil, ErrModelArtifactFileTooLarge
×
UNCOV
982
                        }
×
983

UNCOV
984
                default:
×
UNCOV
985
                        // Ignore non-API sections.
×
UNCOV
986
                        continue
×
987
                }
988
        }
989

990
        return msg, errors.Wrap(msg.Validate(), "api: invalid form parameters")
3✔
991
}
992

993
// deployments
994
func (d *DeploymentsApiHandlers) createDeployment(
995
        c *gin.Context,
996
        ctx context.Context,
997
        group string,
998
) {
3✔
999
        constructor, err := d.getDeploymentConstructorFromBody(c, group)
3✔
1000
        if err != nil {
6✔
1001
                d.view.RenderError(
3✔
1002
                        c,
3✔
1003
                        errors.Wrap(err, "Validating request body"),
3✔
1004
                        http.StatusBadRequest,
3✔
1005
                )
3✔
1006
                return
3✔
1007
        }
3✔
1008

1009
        id, err := d.app.CreateDeployment(ctx, constructor)
3✔
1010
        switch err {
3✔
1011
        case nil:
3✔
1012
                location := fmt.Sprintf("%s/%s", ApiUrlManagementDeployments, id)
3✔
1013
                c.Writer.Header().Add("Location", location)
3✔
1014
                c.Status(http.StatusCreated)
3✔
1015
        case app.ErrNoArtifact:
1✔
1016
                d.view.RenderError(c, err, http.StatusUnprocessableEntity)
1✔
1017
        case app.ErrNoDevices:
1✔
1018
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1019
        case app.ErrConflictingDeployment:
2✔
1020
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1021
        default:
1✔
1022
                d.view.RenderInternalError(c, err)
1✔
1023
        }
1024
}
1025

1026
func (d *DeploymentsApiHandlers) PostDeployment(c *gin.Context) {
3✔
1027
        ctx := c.Request.Context()
3✔
1028

3✔
1029
        d.createDeployment(c, ctx, "")
3✔
1030
}
3✔
1031

1032
func (d *DeploymentsApiHandlers) DeployToGroup(c *gin.Context) {
2✔
1033
        ctx := c.Request.Context()
2✔
1034

2✔
1035
        group := c.Param("name")
2✔
1036
        if len(group) < 1 {
2✔
UNCOV
1037
                d.view.RenderError(c, ErrMissingGroupName, http.StatusBadRequest)
×
UNCOV
1038
        }
×
1039
        d.createDeployment(c, ctx, group)
2✔
1040
}
1041

1042
// parseDeviceConfigurationDeploymentPathParams parses expected params
1043
// and check if the params are not empty
1044
func parseDeviceConfigurationDeploymentPathParams(c *gin.Context) (string, string, string, error) {
3✔
1045
        tenantID := c.Param("tenant")
3✔
1046
        deviceID := c.Param(ParamDeviceID)
3✔
1047
        if deviceID == "" {
3✔
UNCOV
1048
                return "", "", "", errors.New("device ID missing")
×
1049
        }
×
1050
        deploymentID := c.Param(ParamDeploymentID)
3✔
1051
        if deploymentID == "" {
3✔
UNCOV
1052
                return "", "", "", errors.New("deployment ID missing")
×
UNCOV
1053
        }
×
1054
        return tenantID, deviceID, deploymentID, nil
3✔
1055
}
1056

1057
// getConfigurationDeploymentConstructorFromBody extracts configuration
1058
// deployment constructor from the request body and validates it
1059
func getConfigurationDeploymentConstructorFromBody(c *gin.Context) (
1060
        *model.ConfigurationDeploymentConstructor, error) {
3✔
1061

3✔
1062
        var constructor *model.ConfigurationDeploymentConstructor
3✔
1063

3✔
1064
        if err := c.ShouldBindJSON(&constructor); err != nil {
5✔
1065
                return nil, err
2✔
1066
        }
2✔
1067

1068
        if err := constructor.Validate(); err != nil {
4✔
1069
                return nil, err
2✔
1070
        }
2✔
1071

1072
        return constructor, nil
2✔
1073
}
1074

1075
// device configuration deployment handler
1076
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1077
        c *gin.Context,
1078
) {
3✔
1079

3✔
1080
        // get path params
3✔
1081
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(c)
3✔
1082
        if err != nil {
3✔
UNCOV
1083
                d.view.RenderError(c, err, http.StatusBadRequest)
×
UNCOV
1084
                return
×
UNCOV
1085
        }
×
1086

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

3✔
1090
        constructor, err := getConfigurationDeploymentConstructorFromBody(c)
3✔
1091
        if err != nil {
6✔
1092
                d.view.RenderError(
3✔
1093
                        c,
3✔
1094
                        errors.Wrap(err, "Validating request body"),
3✔
1095
                        http.StatusBadRequest,
3✔
1096
                )
3✔
1097
                return
3✔
1098
        }
3✔
1099

1100
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
2✔
1101
        switch err {
2✔
1102
        default:
1✔
1103
                d.view.RenderInternalError(c, err)
1✔
1104
        case nil:
2✔
1105
                c.Request.URL.Path = "./deployments"
2✔
1106
                d.view.RenderSuccessPost(c, id)
2✔
1107
        case app.ErrDuplicateDeployment:
2✔
1108
                d.view.RenderError(c, err, http.StatusConflict)
2✔
1109
        case app.ErrInvalidDeploymentID:
1✔
1110
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1111
        }
1112
}
1113

1114
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1115
        c *gin.Context,
1116
        group string,
1117
) (*model.DeploymentConstructor, error) {
3✔
1118
        var constructor *model.DeploymentConstructor
3✔
1119
        if err := c.ShouldBindJSON(&constructor); err != nil {
4✔
1120
                return nil, err
1✔
1121
        }
1✔
1122

1123
        constructor.Group = group
3✔
1124

3✔
1125
        if err := constructor.ValidateNew(); err != nil {
6✔
1126
                return nil, err
3✔
1127
        }
3✔
1128

1129
        return constructor, nil
3✔
1130
}
1131

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

2✔
1135
        id := c.Param("id")
2✔
1136

2✔
1137
        if !govalidator.IsUUID(id) {
3✔
1138
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
1✔
1139
                return
1✔
1140
        }
1✔
1141

1142
        deployment, err := d.app.GetDeployment(ctx, id)
2✔
1143
        if err != nil {
2✔
UNCOV
1144
                d.view.RenderInternalError(c, err)
×
UNCOV
1145
                return
×
UNCOV
1146
        }
×
1147

1148
        if deployment == nil {
2✔
UNCOV
1149
                d.view.RenderErrorNotFound(c)
×
UNCOV
1150
                return
×
UNCOV
1151
        }
×
1152

1153
        d.view.RenderSuccessGet(c, deployment)
2✔
1154
}
1155

1156
func (d *DeploymentsApiHandlers) GetDeploymentStats(c *gin.Context) {
1✔
1157
        ctx := c.Request.Context()
1✔
1158

1✔
1159
        id := c.Param("id")
1✔
1160

1✔
1161
        if !govalidator.IsUUID(id) {
1✔
1162
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
1163
                return
×
UNCOV
1164
        }
×
1165

1166
        stats, err := d.app.GetDeploymentStats(ctx, id)
1✔
1167
        if err != nil {
1✔
UNCOV
1168
                d.view.RenderInternalError(c, err)
×
UNCOV
1169
                return
×
UNCOV
1170
        }
×
1171

1172
        if stats == nil {
1✔
UNCOV
1173
                d.view.RenderErrorNotFound(c)
×
1174
                return
×
1175
        }
×
1176

1177
        d.view.RenderSuccessGet(c, stats)
1✔
1178
}
1179

1180
func (d *DeploymentsApiHandlers) GetDeploymentsStats(c *gin.Context) {
1✔
1181

1✔
1182
        ctx := c.Request.Context()
1✔
1183

1✔
1184
        ids := model.DeploymentIDs{}
1✔
1185
        if err := c.ShouldBindJSON(&ids); err != nil {
1✔
1186
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1187
                return
×
UNCOV
1188
        }
×
1189

1190
        if len(ids.IDs) == 0 {
1✔
UNCOV
1191
                c.JSON(http.StatusOK, struct{}{})
×
UNCOV
1192
                return
×
UNCOV
1193
        }
×
1194

1195
        if err := ids.Validate(); err != nil {
2✔
1196
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1197
                return
1✔
1198
        }
1✔
1199

1200
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
1✔
1201
        if err != nil {
2✔
1202
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
2✔
1203
                        d.view.RenderError(c, err, http.StatusNotFound)
1✔
1204
                        return
1✔
1205
                }
1✔
1206
                d.view.RenderInternalError(c, err)
1✔
1207
                return
1✔
1208
        }
1209

1210
        c.JSON(http.StatusOK, stats)
1✔
1211
}
1212

UNCOV
1213
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(c *gin.Context) {
×
UNCOV
1214
        ctx := c.Request.Context()
×
UNCOV
1215

×
UNCOV
1216
        id := c.Param("id")
×
UNCOV
1217

×
UNCOV
1218
        if !govalidator.IsUUID(id) {
×
UNCOV
1219
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
UNCOV
1220
                return
×
UNCOV
1221
        }
×
1222

UNCOV
1223
        deployment, err := d.app.GetDeployment(ctx, id)
×
UNCOV
1224
        if err != nil {
×
1225
                d.view.RenderInternalError(c, err)
×
1226
                return
×
1227
        }
×
1228

1229
        if deployment == nil {
×
1230
                d.view.RenderErrorNotFound(c)
×
1231
                return
×
1232
        }
×
1233

UNCOV
1234
        d.view.RenderSuccessGet(c, deployment.DeviceList)
×
1235
}
1236

1237
func (d *DeploymentsApiHandlers) AbortDeployment(c *gin.Context) {
1✔
1238
        ctx := c.Request.Context()
1✔
1239

1✔
1240
        id := c.Param("id")
1✔
1241

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

1247
        // receive request body
1248
        var status struct {
1✔
1249
                Status model.DeviceDeploymentStatus
1✔
1250
        }
1✔
1251

1✔
1252
        err := c.ShouldBindJSON(&status)
1✔
1253
        if err != nil {
1✔
UNCOV
1254
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1255
                return
×
1256
        }
×
1257
        // "aborted" is the only supported status
1258
        if status.Status != model.DeviceDeploymentStatusAborted {
1✔
UNCOV
1259
                d.view.RenderError(c, ErrUnexpectedDeploymentStatus, http.StatusBadRequest)
×
UNCOV
1260
        }
×
1261

1262
        l := log.FromContext(ctx)
1✔
1263
        l.Infof("Abort deployment: %s", id)
1✔
1264

1✔
1265
        // Check if deployment is finished
1✔
1266
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
1✔
1267
        if err != nil {
1✔
1268
                d.view.RenderInternalError(c, err)
×
UNCOV
1269
                return
×
UNCOV
1270
        }
×
1271
        if isDeploymentFinished {
2✔
1272
                d.view.RenderError(c, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity)
1✔
1273
                return
1✔
1274
        }
1✔
1275

1276
        // Abort deployments for devices and update deployment stats
1277
        if err := d.app.AbortDeployment(ctx, id); err != nil {
1✔
UNCOV
1278
                d.view.RenderInternalError(c, err)
×
UNCOV
1279
        }
×
1280

1281
        d.view.RenderEmptySuccessResponse(c)
1✔
1282
}
1283

1284
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(c *gin.Context) {
3✔
1285
        var (
3✔
1286
                installed *model.InstalledDeviceDeployment
3✔
1287
                ctx       = c.Request.Context()
3✔
1288
                idata     = identity.FromContext(ctx)
3✔
1289
        )
3✔
1290
        if idata == nil {
4✔
1291
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
1✔
1292
                return
1✔
1293
        }
1✔
1294

1295
        q := c.Request.URL.Query()
3✔
1296
        defer func() {
6✔
1297
                var reEncode bool = false
3✔
1298
                if name := q.Get(ParamArtifactName); name != "" {
6✔
1299
                        q.Set(ParamArtifactName, Redacted)
3✔
1300
                        reEncode = true
3✔
1301
                }
3✔
1302
                if typ := q.Get(ParamDeviceType); typ != "" {
6✔
1303
                        q.Set(ParamDeviceType, Redacted)
3✔
1304
                        reEncode = true
3✔
1305
                }
3✔
1306
                if reEncode {
6✔
1307
                        c.Request.URL.RawQuery = q.Encode()
3✔
1308
                }
3✔
1309
        }()
1310
        if strings.EqualFold(c.Request.Method, http.MethodPost) {
5✔
1311
                // POST
2✔
1312
                installed = new(model.InstalledDeviceDeployment)
2✔
1313
                if err := c.ShouldBindJSON(&installed); err != nil {
3✔
1314
                        d.view.RenderError(c,
1✔
1315
                                errors.Wrap(err, "invalid schema"),
1✔
1316
                                http.StatusBadRequest)
1✔
1317
                        return
1✔
1318
                }
1✔
1319
        } else {
3✔
1320
                // GET or HEAD
3✔
1321
                installed = &model.InstalledDeviceDeployment{
3✔
1322
                        ArtifactName: q.Get(ParamArtifactName),
3✔
1323
                        DeviceType:   q.Get(ParamDeviceType),
3✔
1324
                }
3✔
1325
        }
3✔
1326

1327
        if err := installed.Validate(); err != nil {
4✔
1328
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1329
                return
1✔
1330
        }
1✔
1331

1332
        request := &model.DeploymentNextRequest{
3✔
1333
                DeviceProvides: installed,
3✔
1334
        }
3✔
1335

3✔
1336
        d.getDeploymentForDevice(c, idata, request)
3✔
1337
}
1338

1339
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1340
        c *gin.Context,
1341
        idata *identity.Identity,
1342
        request *model.DeploymentNextRequest,
1343
) {
3✔
1344
        ctx := c.Request.Context()
3✔
1345

3✔
1346
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
3✔
1347
        if err != nil {
5✔
1348
                if err == app.ErrConflictingRequestData {
3✔
1349
                        d.view.RenderError(c, err, http.StatusConflict)
1✔
1350
                } else {
2✔
1351
                        d.view.RenderInternalError(c, err)
1✔
1352
                }
1✔
1353
                return
2✔
1354
        }
1355

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

1394
        d.view.RenderSuccessGet(c, deployment)
3✔
1395
}
1396

1397
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1398
        c *gin.Context,
1399
) {
2✔
1400
        ctx := c.Request.Context()
2✔
1401

2✔
1402
        did := c.Param("id")
2✔
1403

2✔
1404
        idata := identity.FromContext(ctx)
2✔
1405
        if idata == nil {
2✔
UNCOV
1406
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
UNCOV
1407
                return
×
UNCOV
1408
        }
×
1409

1410
        // receive request body
1411
        var report model.StatusReport
2✔
1412

2✔
1413
        err := c.ShouldBindJSON(&report)
2✔
1414
        if err != nil {
3✔
1415
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1416
                return
1✔
1417
        }
1✔
1418
        l := log.FromContext(ctx)
2✔
1419
        l.Infof("status: %+v", report)
2✔
1420
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
2✔
1421
                idata.Subject, model.DeviceDeploymentState{
2✔
1422
                        Status:   report.Status,
2✔
1423
                        SubState: report.SubState,
2✔
1424
                }); err != nil {
3✔
1425

1✔
1426
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
1✔
UNCOV
1427
                        d.view.RenderError(c, err, http.StatusConflict)
×
1428
                } else if err == app.ErrStorageNotFound {
2✔
1429
                        d.view.RenderErrorNotFound(c)
1✔
1430
                } else {
1✔
UNCOV
1431
                        d.view.RenderInternalError(c, err)
×
UNCOV
1432
                }
×
1433
                return
1✔
1434
        }
1435

1436
        d.view.RenderEmptySuccessResponse(c)
2✔
1437
}
1438

1439
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1440
        c *gin.Context,
1441
) {
2✔
1442
        ctx := c.Request.Context()
2✔
1443

2✔
1444
        did := c.Param("id")
2✔
1445

2✔
1446
        if !govalidator.IsUUID(did) {
2✔
UNCOV
1447
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
UNCOV
1448
                return
×
UNCOV
1449
        }
×
1450

1451
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
2✔
1452
        if err != nil {
2✔
UNCOV
1453
                switch err {
×
UNCOV
1454
                case app.ErrModelDeploymentNotFound:
×
UNCOV
1455
                        d.view.RenderError(c, err, http.StatusNotFound)
×
UNCOV
1456
                        return
×
UNCOV
1457
                default:
×
UNCOV
1458
                        d.view.RenderInternalError(c, err)
×
1459
                        return
×
1460
                }
1461
        }
1462

1463
        d.view.RenderSuccessGet(c, statuses)
2✔
1464
}
1465

1466
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1467
        c *gin.Context,
1468
) {
1✔
1469
        ctx := c.Request.Context()
1✔
1470

1✔
1471
        did := c.Param("id")
1✔
1472

1✔
1473
        if !govalidator.IsUUID(did) {
1✔
UNCOV
1474
                d.view.RenderError(c, ErrIDNotUUID, http.StatusBadRequest)
×
UNCOV
1475
                return
×
UNCOV
1476
        }
×
1477

1478
        page, perPage, err := rest.ParsePagingParameters(c.Request)
1✔
1479
        if err != nil {
1✔
UNCOV
1480
                d.view.RenderError(c, err, http.StatusBadRequest)
×
UNCOV
1481
                return
×
UNCOV
1482
        }
×
1483

1484
        lq := store.ListQuery{
1✔
1485
                Skip:         int((page - 1) * perPage),
1✔
1486
                Limit:        int(perPage),
1✔
1487
                DeploymentID: did,
1✔
1488
        }
1✔
1489
        if status := c.Request.URL.Query().Get("status"); status != "" {
1✔
UNCOV
1490
                lq.Status = &status
×
UNCOV
1491
        }
×
1492
        if err = lq.Validate(); err != nil {
1✔
1493
                d.view.RenderError(c, err, http.StatusBadRequest)
×
1494
                return
×
UNCOV
1495
        }
×
1496

1497
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
1✔
1498
        if err != nil {
1✔
UNCOV
1499
                switch err {
×
UNCOV
1500
                case app.ErrModelDeploymentNotFound:
×
UNCOV
1501
                        d.view.RenderError(c, err, http.StatusNotFound)
×
1502
                        return
×
1503
                default:
×
UNCOV
1504
                        d.view.RenderInternalError(c, err)
×
1505
                        return
×
1506
                }
1507
        }
1508

1509
        hasNext := totalCount > int(page*perPage)
1✔
1510
        hints := rest.NewPagingHints().
1✔
1511
                SetPage(page).
1✔
1512
                SetPerPage(perPage).
1✔
1513
                SetHasNext(hasNext).
1✔
1514
                SetTotalCount(int64(totalCount))
1✔
1515

1✔
1516
        links, err := rest.MakePagingHeaders(c.Request, hints)
1✔
1517
        if err != nil {
1✔
UNCOV
1518
                d.view.RenderInternalError(c, err)
×
UNCOV
1519
                return
×
UNCOV
1520
        }
×
1521

1522
        for _, l := range links {
2✔
1523
                c.Writer.Header().Add(hdrLink, l)
1✔
1524
        }
1✔
1525
        c.Writer.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
1526
        d.view.RenderSuccessGet(c, statuses)
1✔
1527
}
1528

1529
func ParseLookupQuery(vals url.Values) (model.Query, error) {
3✔
1530
        query := model.Query{}
3✔
1531

3✔
1532
        createdBefore := vals.Get("created_before")
3✔
1533
        if createdBefore != "" {
5✔
1534
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
3✔
1535
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1536
                } else {
2✔
1537
                        query.CreatedBefore = &createdBeforeTime
1✔
1538
                }
1✔
1539
        }
1540

1541
        createdAfter := vals.Get("created_after")
3✔
1542
        if createdAfter != "" {
4✔
1543
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
1✔
UNCOV
1544
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1545
                } else {
1✔
1546
                        query.CreatedAfter = &createdAfterTime
1✔
1547
                }
1✔
1548
        }
1549

1550
        switch strings.ToLower(vals.Get("sort")) {
3✔
1551
        case model.SortDirectionAscending:
1✔
1552
                query.Sort = model.SortDirectionAscending
1✔
1553
        case "", model.SortDirectionDescending:
3✔
1554
                query.Sort = model.SortDirectionDescending
3✔
UNCOV
1555
        default:
×
1556
                return query, ErrInvalidSortDirection
×
1557
        }
1558

1559
        status := vals.Get("status")
3✔
1560
        switch status {
3✔
UNCOV
1561
        case "inprogress":
×
UNCOV
1562
                query.Status = model.StatusQueryInProgress
×
UNCOV
1563
        case "finished":
×
UNCOV
1564
                query.Status = model.StatusQueryFinished
×
UNCOV
1565
        case "pending":
×
UNCOV
1566
                query.Status = model.StatusQueryPending
×
1567
        case "aborted":
×
1568
                query.Status = model.StatusQueryAborted
×
1569
        case "":
3✔
1570
                query.Status = model.StatusQueryAny
3✔
UNCOV
1571
        default:
×
UNCOV
1572
                return query, errors.Errorf("unknown status %s", status)
×
1573

1574
        }
1575

1576
        dType := vals.Get("type")
3✔
1577
        if dType == "" {
6✔
1578
                return query, nil
3✔
1579
        }
3✔
1580
        deploymentType := model.DeploymentType(dType)
×
UNCOV
1581
        if deploymentType == model.DeploymentTypeSoftware ||
×
UNCOV
1582
                deploymentType == model.DeploymentTypeConfiguration {
×
1583
                query.Type = deploymentType
×
1584
        } else {
×
UNCOV
1585
                return query, errors.Errorf("unknown deployment type %s", dType)
×
UNCOV
1586
        }
×
1587

UNCOV
1588
        return query, nil
×
1589
}
1590

1591
func ParseDeploymentLookupQueryV1(vals url.Values) (model.Query, error) {
3✔
1592
        query, err := ParseLookupQuery(vals)
3✔
1593
        if err != nil {
4✔
1594
                return query, err
1✔
1595
        }
1✔
1596

1597
        search := vals.Get("search")
3✔
1598
        if search != "" {
3✔
UNCOV
1599
                query.SearchText = search
×
1600
        }
×
1601

1602
        return query, nil
3✔
1603
}
1604

1605
func ParseDeploymentLookupQueryV2(vals url.Values) (model.Query, error) {
2✔
1606
        query, err := ParseLookupQuery(vals)
2✔
1607
        if err != nil {
2✔
UNCOV
1608
                return query, err
×
UNCOV
1609
        }
×
1610

1611
        query.Names = vals["name"]
2✔
1612
        query.IDs = vals["id"]
2✔
1613

2✔
1614
        return query, nil
2✔
1615
}
1616

1617
func parseEpochToTimestamp(epoch string) (time.Time, error) {
2✔
1618
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
3✔
1619
                return time.Time{}, errors.New("invalid timestamp: " + epoch)
1✔
1620
        } else {
2✔
1621
                return time.Unix(epochInt64, 0).UTC(), nil
1✔
1622
        }
1✔
1623
}
1624

1625
func (d *DeploymentsApiHandlers) LookupDeployment(c *gin.Context) {
3✔
1626
        ctx := c.Request.Context()
3✔
1627
        q := c.Request.URL.Query()
3✔
1628
        defer func() {
6✔
1629
                if search := q.Get("search"); search != "" {
3✔
UNCOV
1630
                        q.Set("search", Redacted)
×
UNCOV
1631
                        c.Request.URL.RawQuery = q.Encode()
×
UNCOV
1632
                }
×
1633
        }()
1634

1635
        query, err := ParseDeploymentLookupQueryV1(q)
3✔
1636
        if err != nil {
4✔
1637
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1638
                return
1✔
1639
        }
1✔
1640

1641
        page, perPage, err := rest.ParsePagingParameters(c.Request)
3✔
1642
        if err != nil {
4✔
1643
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1644
                return
1✔
1645
        }
1✔
1646
        query.Skip = int((page - 1) * perPage)
3✔
1647
        query.Limit = int(perPage + 1)
3✔
1648

3✔
1649
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
3✔
1650
        if err != nil {
4✔
1651
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1652
                return
1✔
1653
        }
1✔
1654
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
3✔
1655

3✔
1656
        len := len(deps)
3✔
1657
        hasNext := false
3✔
1658
        if int64(len) > perPage {
3✔
UNCOV
1659
                hasNext = true
×
UNCOV
1660
                len = int(perPage)
×
UNCOV
1661
        }
×
1662

1663
        hints := rest.NewPagingHints().
3✔
1664
                SetPage(page).
3✔
1665
                SetPerPage(perPage).
3✔
1666
                SetHasNext(hasNext).
3✔
1667
                SetTotalCount(int64(totalCount))
3✔
1668

3✔
1669
        links, err := rest.MakePagingHeaders(c.Request, hints)
3✔
1670
        if err != nil {
3✔
1671
                d.view.RenderInternalError(c, err)
×
1672
                return
×
1673
        }
×
1674
        for _, l := range links {
6✔
1675
                c.Writer.Header().Add(hdrLink, l)
3✔
1676
        }
3✔
1677

1678
        d.view.RenderSuccessGet(c, deps[:len])
3✔
1679
}
1680

1681
func (d *DeploymentsApiHandlers) LookupDeploymentV2(c *gin.Context) {
2✔
1682
        ctx := c.Request.Context()
2✔
1683
        q := c.Request.URL.Query()
2✔
1684
        defer func() {
4✔
1685
                if q.Has("name") {
3✔
1686
                        q["name"] = []string{Redacted}
1✔
1687
                        c.Request.URL.RawQuery = q.Encode()
1✔
1688
                }
1✔
1689
        }()
1690

1691
        query, err := ParseDeploymentLookupQueryV2(q)
2✔
1692
        if err != nil {
2✔
UNCOV
1693
                d.view.RenderError(c, err, http.StatusBadRequest)
×
UNCOV
1694
                return
×
UNCOV
1695
        }
×
1696

1697
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1698
        if err != nil {
3✔
1699
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1700
                return
1✔
1701
        }
1✔
1702
        query.Skip = int((page - 1) * perPage)
2✔
1703
        query.Limit = int(perPage + 1)
2✔
1704

2✔
1705
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
2✔
1706
        if err != nil {
2✔
1707
                d.view.RenderError(c, err, http.StatusBadRequest)
×
UNCOV
1708
                return
×
UNCOV
1709
        }
×
1710
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
2✔
1711

2✔
1712
        len := len(deps)
2✔
1713
        hasNext := false
2✔
1714
        if int64(len) > perPage {
3✔
1715
                hasNext = true
1✔
1716
                len = int(perPage)
1✔
1717
        }
1✔
1718

1719
        hints := rest.NewPagingHints().
2✔
1720
                SetPage(page).
2✔
1721
                SetPerPage(perPage).
2✔
1722
                SetHasNext(hasNext).
2✔
1723
                SetTotalCount(int64(totalCount))
2✔
1724

2✔
1725
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
1726
        if err != nil {
2✔
UNCOV
1727
                d.view.RenderInternalError(c, err)
×
UNCOV
1728
                return
×
UNCOV
1729
        }
×
1730
        for _, l := range links {
4✔
1731
                c.Writer.Header().Add(hdrLink, l)
2✔
1732
        }
2✔
1733

1734
        d.view.RenderSuccessGet(c, deps[:len])
2✔
1735
}
1736

1737
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(c *gin.Context) {
1✔
1738
        ctx := c.Request.Context()
1✔
1739

1✔
1740
        did := c.Param("id")
1✔
1741

1✔
1742
        idata := identity.FromContext(ctx)
1✔
1743
        if idata == nil {
1✔
UNCOV
1744
                d.view.RenderError(c, ErrMissingIdentity, http.StatusBadRequest)
×
UNCOV
1745
                return
×
UNCOV
1746
        }
×
1747

1748
        // reuse DeploymentLog, device and deployment IDs are ignored when
1749
        // (un-)marshaling DeploymentLog to/from JSON
1750
        var log model.DeploymentLog
1✔
1751

1✔
1752
        err := c.ShouldBindJSON(&log)
1✔
1753
        if err != nil {
1✔
UNCOV
1754
                d.view.RenderError(c, err, http.StatusBadRequest)
×
UNCOV
1755
                return
×
1756
        }
×
1757

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

×
UNCOV
1761
                if err == app.ErrModelDeploymentNotFound {
×
UNCOV
1762
                        d.view.RenderError(c, err, http.StatusNotFound)
×
UNCOV
1763
                } else {
×
UNCOV
1764
                        d.view.RenderInternalError(c, err)
×
UNCOV
1765
                }
×
1766
                return
×
1767
        }
1768

1769
        d.view.RenderEmptySuccessResponse(c)
1✔
1770
}
1771

1772
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(c *gin.Context) {
1✔
1773
        ctx := c.Request.Context()
1✔
1774

1✔
1775
        did := c.Param("id")
1✔
1776
        devid := c.Param("devid")
1✔
1777

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

1✔
1780
        if err != nil {
1✔
UNCOV
1781
                d.view.RenderInternalError(c, err)
×
UNCOV
1782
                return
×
UNCOV
1783
        }
×
1784

1785
        if depl == nil {
1✔
UNCOV
1786
                d.view.RenderErrorNotFound(c)
×
UNCOV
1787
                return
×
UNCOV
1788
        }
×
1789

1790
        d.view.RenderDeploymentLog(c, *depl)
1✔
1791
}
1792

1793
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(c *gin.Context) {
1✔
1794
        ctx := c.Request.Context()
1✔
1795

1✔
1796
        id := c.Param("id")
1✔
1797
        err := d.app.AbortDeviceDeployments(ctx, id)
1✔
1798

1✔
1799
        switch err {
1✔
1800
        case nil, app.ErrStorageNotFound:
1✔
1801
                d.view.RenderEmptySuccessResponse(c)
1✔
1802
        default:
1✔
1803
                d.view.RenderInternalError(c, err)
1✔
1804
        }
1805
}
1806

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

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

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

1821
func (d *DeploymentsApiHandlers) ListDeviceDeployments(c *gin.Context) {
2✔
1822
        ctx := c.Request.Context()
2✔
1823
        d.listDeviceDeployments(ctx, c, true)
2✔
1824
}
2✔
1825

1826
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(c *gin.Context) {
2✔
1827
        ctx := c.Request.Context()
2✔
1828
        tenantID := c.Param("tenant")
2✔
1829
        if tenantID != "" {
4✔
1830
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
2✔
1831
                        Tenant:   tenantID,
2✔
1832
                        IsDevice: true,
2✔
1833
                })
2✔
1834
        }
2✔
1835
        d.listDeviceDeployments(ctx, c, true)
2✔
1836
}
1837

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

1850
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1851
        c *gin.Context, byDeviceID bool) {
2✔
1852

2✔
1853
        did := ""
2✔
1854
        var IDs []string
2✔
1855
        if byDeviceID {
4✔
1856
                did = c.Param("id")
2✔
1857
        } else {
4✔
1858
                values := c.Request.URL.Query()
2✔
1859
                if values.Has("id") && len(values["id"]) > 0 {
3✔
1860
                        IDs = values["id"]
1✔
1861
                } else {
3✔
1862
                        d.view.RenderError(c, ErrEmptyID, http.StatusBadRequest)
2✔
1863
                        return
2✔
1864
                }
2✔
1865
        }
1866

1867
        page, perPage, err := rest.ParsePagingParameters(c.Request)
2✔
1868
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
3✔
1869
                err = rest.ErrQueryParmLimit(ParamPerPage)
1✔
1870
        }
1✔
1871
        if err != nil {
3✔
1872
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1873
                return
1✔
1874
        }
1✔
1875

1876
        lq := store.ListQueryDeviceDeployments{
2✔
1877
                Skip:     int((page - 1) * perPage),
2✔
1878
                Limit:    int(perPage),
2✔
1879
                DeviceID: did,
2✔
1880
                IDs:      IDs,
2✔
1881
        }
2✔
1882
        if status := c.Request.URL.Query().Get("status"); status != "" {
3✔
1883
                lq.Status = &status
1✔
1884
        }
1✔
1885
        if err = lq.Validate(); err != nil {
3✔
1886
                d.view.RenderError(c, err, http.StatusBadRequest)
1✔
1887
                return
1✔
1888
        }
1✔
1889

1890
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
2✔
1891
        if err != nil {
3✔
1892
                d.view.RenderInternalError(c, err)
1✔
1893
                return
1✔
1894
        }
1✔
1895
        c.Writer.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
2✔
1896

2✔
1897
        hasNext := totalCount > lq.Skip+len(deps)
2✔
1898

2✔
1899
        hints := rest.NewPagingHints().
2✔
1900
                SetPage(page).
2✔
1901
                SetPerPage(perPage).
2✔
1902
                SetHasNext(hasNext).
2✔
1903
                SetTotalCount(int64(totalCount))
2✔
1904

2✔
1905
        links, err := rest.MakePagingHeaders(c.Request, hints)
2✔
1906
        if err != nil {
2✔
UNCOV
1907
                rest.RenderInternalError(c, err)
×
UNCOV
1908
                return
×
UNCOV
1909
        }
×
1910
        for _, l := range links {
4✔
1911
                c.Writer.Header().Add(hdrLink, l)
2✔
1912
        }
2✔
1913

1914
        d.view.RenderSuccessGet(c, deps)
2✔
1915
}
1916

1917
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(c *gin.Context) {
1✔
1918
        ctx := c.Request.Context()
1✔
1919
        tenantID := c.Param("tenantID")
1✔
1920
        if tenantID != "" {
1✔
1921
                ctx = identity.WithContext(c.Request.Context(), &identity.Identity{
×
UNCOV
1922
                        Tenant:   tenantID,
×
UNCOV
1923
                        IsDevice: true,
×
UNCOV
1924
                })
×
UNCOV
1925
        }
×
1926

1927
        id := c.Param("id")
1✔
1928

1✔
1929
        // Decommission deployments for devices and update deployment stats
1✔
1930
        err := d.app.DecommissionDevice(ctx, id)
1✔
1931

1✔
1932
        switch err {
1✔
1933
        case nil, app.ErrStorageNotFound:
1✔
1934
                d.view.RenderEmptySuccessResponse(c)
1✔
1935
        default:
×
1936
                d.view.RenderInternalError(c, err)
×
1937

1938
        }
1939
}
1940

1941
// tenants
1942

1943
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(c *gin.Context) {
2✔
1944
        ctx := c.Request.Context()
2✔
1945

2✔
1946
        defer c.Request.Body.Close()
2✔
1947

2✔
1948
        tenant, err := model.ParseNewTenantReq(c.Request.Body)
2✔
1949
        if err != nil {
4✔
1950
                d.view.RenderError(c, err, http.StatusBadRequest)
2✔
1951
                return
2✔
1952
        }
2✔
1953

1954
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1955
        if err != nil {
1✔
UNCOV
1956
                d.view.RenderInternalError(c, err)
×
UNCOV
1957
                return
×
UNCOV
1958
        }
×
1959

1960
        c.Status(http.StatusCreated)
1✔
1961
}
1962

1963
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1964
        c *gin.Context,
1965
) {
2✔
1966
        tenantID := c.Param("tenant")
2✔
1967
        if tenantID == "" {
3✔
1968

1✔
1969
                d.view.RenderError(c, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1970
                return
1✔
1971
        }
1✔
1972
        c.Request = c.Request.WithContext(identity.WithContext(
2✔
1973
                c.Request.Context(),
2✔
1974
                &identity.Identity{Tenant: tenantID},
2✔
1975
        ))
2✔
1976
        d.LookupDeployment(c)
2✔
1977
}
1978

1979
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1980
        c *gin.Context,
1981
) {
3✔
1982

3✔
1983
        tenantID := c.Param("tenant")
3✔
1984

3✔
1985
        ctx := identity.WithContext(
3✔
1986
                c.Request.Context(),
3✔
1987
                &identity.Identity{Tenant: tenantID},
3✔
1988
        )
3✔
1989

3✔
1990
        settings, err := d.app.GetStorageSettings(ctx)
3✔
1991
        if err != nil {
4✔
1992
                d.view.RenderInternalError(c, err)
1✔
1993
                return
1✔
1994
        }
1✔
1995

1996
        d.view.RenderSuccessGet(c, settings)
3✔
1997
}
1998

1999
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
2000
        c *gin.Context,
2001
) {
3✔
2002

3✔
2003
        defer c.Request.Body.Close()
3✔
2004

3✔
2005
        tenantID := c.Param("tenant")
3✔
2006

3✔
2007
        ctx := identity.WithContext(
3✔
2008
                c.Request.Context(),
3✔
2009
                &identity.Identity{Tenant: tenantID},
3✔
2010
        )
3✔
2011

3✔
2012
        settings, err := model.ParseStorageSettingsRequest(c.Request.Body)
3✔
2013
        if err != nil {
6✔
2014
                d.view.RenderError(c, err, http.StatusBadRequest)
3✔
2015
                return
3✔
2016
        }
3✔
2017

2018
        err = d.app.SetStorageSettings(ctx, settings)
2✔
2019
        if err != nil {
3✔
2020
                d.view.RenderInternalError(c, err)
1✔
2021
                return
1✔
2022
        }
1✔
2023

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