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

mendersoftware / mender-server / 1899590412

01 Jul 2025 09:26AM UTC coverage: 65.694% (-0.04%) from 65.731%
1899590412

push

gitlab-ci

bahaa-ghazal
refactor: Remove unused ant0ine/go-json-rest utilities from deployments

Signed-off-by: Bahaa Aldeen Ghazal <bahaa.ghazal@northern.tech>

32420 of 49350 relevant lines covered (65.69%)

1.39 hits per line

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

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

15
package http
16

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

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

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

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

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

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

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

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

86
const Redacted = "REDACTED"
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

310
        return filter
3✔
311
}
312

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

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

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

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

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

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

341
// images
342

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

459
const maxMetadataSize = 2048
460

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
610
        id := c.Param("id")
×
611

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

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

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

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

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

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

×
647
        var constructor *model.ImageMeta
×
648

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

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

657
        return constructor, nil
×
658
}
659

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

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

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

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

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

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

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

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

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

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

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

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

2✔
748
        // handle specific cases
2✔
749

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1073
        return constructor, nil
2✔
1074
}
1075

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

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

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

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

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

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

1124
        constructor.Group = group
3✔
1125

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

1130
        return constructor, nil
3✔
1131
}
1132

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
1217
        id := c.Param("id")
×
1218

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1575
        }
1576

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

1589
        return query, nil
×
1590
}
1591

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

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

1603
        return query, nil
3✔
1604
}
1605

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1839
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(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, false)
2✔
1849
}
1850

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

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

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

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

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

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

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

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

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

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

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

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

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

1939
        }
1940
}
1941

1942
// tenants
1943

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc