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

mendersoftware / mender-server / 1622978334

13 Jan 2025 03:51PM UTC coverage: 72.802% (-3.8%) from 76.608%
1622978334

Pull #300

gitlab-ci

alfrunes
fix: Deployment device count should not exceed max devices

Added a condition to skip deployments when the device count reaches max
devices.

Changelog: Title
Ticket: MEN-7847
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #300: fix: Deployment device count should not exceed max devices

4251 of 6164 branches covered (68.96%)

Branch coverage included in aggregate %.

0 of 18 new or added lines in 1 file covered. (0.0%)

2544 existing lines in 83 files now uncovered.

42741 of 58384 relevant lines covered (73.21%)

21.49 hits per line

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

61.56
/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/ant0ine/go-json-rest/rest"
30
        "github.com/asaskevich/govalidator"
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/requestlog"
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
func init() {
2✔
48
        rest.ErrorFieldName = "error"
2✔
49
}
2✔
50

51
const (
52
        // 15 minutes
53
        DefaultDownloadLinkExpire = 15 * time.Minute
54
        // 10 Mb
55
        DefaultMaxMetaSize         = 1024 * 1024 * 10
56
        DefaultMaxImageSize        = 10 * 1024 * 1024 * 1024 // 10GiB
57
        DefaultMaxGenerateDataSize = 512 * 1024 * 1024       // 512MiB
58

59
        // Pagination
60
        DefaultPerPage                      = 20
61
        MaximumPerPage                      = 500
62
        MaximumPerPageListDeviceDeployments = 20
63
)
64

65
const (
66
        // Header Constants
67
        hdrTotalCount    = "X-Total-Count"
68
        hdrLink          = "Link"
69
        hdrForwardedHost = "X-Forwarded-Host"
70
)
71

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

90
const Redacted = "REDACTED"
91

92
// JWT token
93
const (
94
        HTTPHeaderAuthorization       = "Authorization"
95
        HTTPHeaderAuthorizationBearer = "Bearer"
96
)
97

98
const (
99
        defaultTimeout = time.Second * 10
100
)
101

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

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

127
        ErrInvalidSortDirection = fmt.Errorf("invalid form value: must be one of \"%s\" or \"%s\"",
128
                model.SortDirectionAscending, model.SortDirectionDescending)
129
)
130

131
type Config struct {
132
        // URL signing parameters:
133

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

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

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

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

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

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

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

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

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

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

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

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

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

210
type DeploymentsApiHandlers struct {
211
        view   RESTView
212
        store  store.DataStore
213
        app    app.App
214
        config Config
215
}
216

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

258
func (d *DeploymentsApiHandlers) AliveHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
259
        w.WriteHeader(http.StatusNoContent)
1✔
260
}
1✔
261

262
func (d *DeploymentsApiHandlers) HealthHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
263
        ctx := r.Context()
1✔
264
        l := log.FromContext(ctx)
1✔
265
        ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
1✔
266
        defer cancel()
1✔
267

1✔
268
        err := d.app.HealthCheck(ctx)
1✔
269
        if err != nil {
2✔
270
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusServiceUnavailable)
1✔
271
                return
1✔
272
        }
1✔
273
        w.WriteHeader(http.StatusNoContent)
1✔
274
}
275

276
func getReleaseOrImageFilter(r *rest.Request, version listReleasesVersion,
277
        paginated bool) *model.ReleaseOrImageFilter {
1✔
278

1✔
279
        q := r.URL.Query()
1✔
280

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

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

315
        return filter
1✔
316
}
317

318
type limitResponse struct {
319
        Limit uint64 `json:"limit"`
320
        Usage uint64 `json:"usage"`
321
}
322

323
func (d *DeploymentsApiHandlers) GetLimit(w rest.ResponseWriter, r *rest.Request) {
1✔
324
        l := requestlog.GetRequestLogger(r)
1✔
325

1✔
326
        name := r.PathParam("name")
1✔
327

1✔
328
        if !model.IsValidLimit(name) {
2✔
329
                d.view.RenderError(w, r,
1✔
330
                        errors.Errorf("unsupported limit %s", name),
1✔
331
                        http.StatusBadRequest, l)
1✔
332
                return
1✔
333
        }
1✔
334

335
        limit, err := d.app.GetLimit(r.Context(), name)
1✔
336
        if err != nil {
2✔
337
                d.view.RenderInternalError(w, r, err, l)
1✔
338
                return
1✔
339
        }
1✔
340

341
        d.view.RenderSuccessGet(w, limitResponse{
1✔
342
                Limit: limit.Value,
1✔
343
                Usage: 0, // TODO fill this when ready
1✔
344
        })
1✔
345
}
346

347
// images
348

UNCOV
349
func (d *DeploymentsApiHandlers) GetImage(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
350
        l := requestlog.GetRequestLogger(r)
×
UNCOV
351

×
UNCOV
352
        id := r.PathParam("id")
×
UNCOV
353

×
UNCOV
354
        if !govalidator.IsUUID(id) {
×
UNCOV
355
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
UNCOV
356
                return
×
UNCOV
357
        }
×
358

UNCOV
359
        image, err := d.app.GetImage(r.Context(), id)
×
UNCOV
360
        if err != nil {
×
361
                d.view.RenderInternalError(w, r, err, l)
×
362
                return
×
363
        }
×
364

UNCOV
365
        if image == nil {
×
UNCOV
366
                d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
367
                return
×
UNCOV
368
        }
×
369

UNCOV
370
        d.view.RenderSuccessGet(w, image)
×
371
}
372

373
func (d *DeploymentsApiHandlers) GetImages(w rest.ResponseWriter, r *rest.Request) {
1✔
374
        l := requestlog.GetRequestLogger(r)
1✔
375

1✔
376
        defer redactReleaseName(r)
1✔
377
        filter := getReleaseOrImageFilter(r, listReleasesV1, false)
1✔
378

1✔
379
        list, _, err := d.app.ListImages(r.Context(), filter)
1✔
380
        if err != nil {
2✔
381
                d.view.RenderInternalError(w, r, err, l)
1✔
382
                return
1✔
383
        }
1✔
384

385
        d.view.RenderSuccessGet(w, list)
1✔
386
}
387

388
func (d *DeploymentsApiHandlers) ListImages(w rest.ResponseWriter, r *rest.Request) {
1✔
389
        l := requestlog.GetRequestLogger(r)
1✔
390

1✔
391
        defer redactReleaseName(r)
1✔
392
        filter := getReleaseOrImageFilter(r, listReleasesV1, true)
1✔
393

1✔
394
        list, totalCount, err := d.app.ListImages(r.Context(), filter)
1✔
395
        if err != nil {
2✔
396
                d.view.RenderInternalError(w, r, err, l)
1✔
397
                return
1✔
398
        }
1✔
399

400
        hasNext := totalCount > int(filter.Page*filter.PerPage)
1✔
401
        links := rest_utils.MakePageLinkHdrs(r, uint64(filter.Page), uint64(filter.PerPage), hasNext)
1✔
402
        for _, l := range links {
2✔
403
                w.Header().Add(hdrLink, l)
1✔
404
        }
1✔
405
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
1✔
406

1✔
407
        d.view.RenderSuccessGet(w, list)
1✔
408
}
409

UNCOV
410
func (d *DeploymentsApiHandlers) DownloadLink(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
411
        l := requestlog.GetRequestLogger(r)
×
UNCOV
412

×
UNCOV
413
        id := r.PathParam("id")
×
UNCOV
414

×
UNCOV
415
        if !govalidator.IsUUID(id) {
×
416
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
417
                return
×
418
        }
×
419

UNCOV
420
        expireSeconds := config.Config.GetInt(dconfig.SettingsStorageDownloadExpireSeconds)
×
UNCOV
421
        link, err := d.app.DownloadLink(r.Context(), id, time.Duration(expireSeconds)*time.Second)
×
UNCOV
422
        if err != nil {
×
423
                d.view.RenderInternalError(w, r, err, l)
×
424
                return
×
425
        }
×
426

UNCOV
427
        if link == nil {
×
428
                d.view.RenderErrorNotFound(w, r, l)
×
429
                return
×
430
        }
×
431

UNCOV
432
        d.view.RenderSuccessGet(w, link)
×
433
}
434

435
func (d *DeploymentsApiHandlers) UploadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
436
        l := requestlog.GetRequestLogger(r)
1✔
437

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

449
        if link == nil {
2✔
450
                d.view.RenderErrorNotFound(w, r, l)
1✔
451
                return
1✔
452
        }
1✔
453

454
        d.view.RenderSuccessGet(w, link)
1✔
455
}
456

457
const maxMetadataSize = 2048
458

459
func (d *DeploymentsApiHandlers) CompleteUpload(w rest.ResponseWriter, r *rest.Request) {
1✔
460
        ctx := r.Context()
1✔
461
        l := log.FromContext(ctx)
1✔
462

1✔
463
        artifactID := r.PathParam(ParamID)
1✔
464

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

485
        err := d.app.CompleteUpload(ctx, artifactID, d.config.EnableDirectUploadSkipVerify, metadata)
1✔
486
        switch errors.Cause(err) {
1✔
487
        case nil:
1✔
488
                // w.Header().Set("Link", "FEAT: Upload status API")
1✔
489
                w.WriteHeader(http.StatusAccepted)
1✔
490
        case app.ErrUploadNotFound:
1✔
491
                d.view.RenderErrorNotFound(w, r, l)
1✔
492
        default:
1✔
493
                l.Error(err)
1✔
494
                w.WriteHeader(http.StatusInternalServerError)
1✔
495
                w.WriteJson(rest_utils.ApiError{ // nolint:errcheck
1✔
496
                        Err:   "internal server error",
1✔
497
                        ReqId: requestid.FromContext(ctx),
1✔
498
                })
1✔
499
        }
500
}
501

502
func (d *DeploymentsApiHandlers) DownloadConfiguration(w rest.ResponseWriter, r *rest.Request) {
1✔
503
        if d.config.PresignSecret == nil {
2✔
504
                rest.NotFound(w, r)
1✔
505
                return
1✔
506
        }
1✔
507
        var (
1✔
508
                deviceID, _     = url.PathUnescape(r.PathParam(ParamDeviceID))
1✔
509
                deviceType, _   = url.PathUnescape(r.PathParam(ParamDeviceType))
1✔
510
                deploymentID, _ = url.PathUnescape(r.PathParam(ParamDeploymentID))
1✔
511
        )
1✔
512
        if deviceID == "" || deviceType == "" || deploymentID == "" {
1✔
513
                rest.NotFound(w, r)
×
514
                return
×
515
        }
×
516

517
        var (
1✔
518
                tenantID string
1✔
519
                l        = log.FromContext(r.Context())
1✔
520
                q        = r.URL.Query()
1✔
521
                err      error
1✔
522
        )
1✔
523
        tenantID = q.Get(ParamTenantID)
1✔
524
        sig := model.NewRequestSignature(r.Request, d.config.PresignSecret)
1✔
525
        if err = sig.Validate(); err != nil {
2✔
526
                switch cause := errors.Cause(err); cause {
1✔
527
                case model.ErrLinkExpired:
1✔
528
                        d.view.RenderError(w, r, cause, http.StatusForbidden, l)
1✔
529
                default:
1✔
530
                        d.view.RenderError(w, r,
1✔
531
                                errors.Wrap(err, "invalid request parameters"),
1✔
532
                                http.StatusBadRequest, l,
1✔
533
                        )
1✔
534
                }
535
                return
1✔
536
        }
537

538
        if !sig.VerifyHMAC256() {
2✔
539
                d.view.RenderError(w, r,
1✔
540
                        errors.New("signature invalid"),
1✔
541
                        http.StatusForbidden, l,
1✔
542
                )
1✔
543
                return
1✔
544
        }
1✔
545

546
        // Validate request signature
547
        ctx := identity.WithContext(r.Context(), &identity.Identity{
1✔
548
                Subject:  deviceID,
1✔
549
                Tenant:   tenantID,
1✔
550
                IsDevice: true,
1✔
551
        })
1✔
552

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

577
        rw := w.(http.ResponseWriter)
1✔
578
        hdr := rw.Header()
1✔
579
        hdr.Set("Content-Disposition", `attachment; filename="artifact.mender"`)
1✔
580
        hdr.Set("Content-Type", app.ArtifactContentType)
1✔
581
        hdr.Set("Content-Length", strconv.Itoa(len(artifactPayload)))
1✔
582
        rw.WriteHeader(http.StatusOK)
1✔
583
        _, err = rw.Write(artifactPayload)
1✔
584
        if err != nil {
1✔
585
                // There's not anything we can do here in terms of the response.
×
586
                l.Error(err.Error())
×
587
        }
×
588
}
589

UNCOV
590
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
591
        l := requestlog.GetRequestLogger(r)
×
UNCOV
592

×
UNCOV
593
        id := r.PathParam("id")
×
UNCOV
594

×
UNCOV
595
        if !govalidator.IsUUID(id) {
×
596
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
597
                return
×
598
        }
×
599

UNCOV
600
        if err := d.app.DeleteImage(r.Context(), id); err != nil {
×
UNCOV
601
                switch err {
×
602
                default:
×
603
                        d.view.RenderInternalError(w, r, err, l)
×
604
                case app.ErrImageMetaNotFound:
×
605
                        d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
606
                case app.ErrModelImageInActiveDeployment:
×
UNCOV
607
                        d.view.RenderError(w, r, ErrArtifactUsedInActiveDeployment, http.StatusConflict, l)
×
608
                }
UNCOV
609
                return
×
610
        }
611

UNCOV
612
        d.view.RenderSuccessDelete(w)
×
613
}
614

615
func (d *DeploymentsApiHandlers) EditImage(w rest.ResponseWriter, r *rest.Request) {
×
616
        l := requestlog.GetRequestLogger(r)
×
617

×
618
        id := r.PathParam("id")
×
619

×
620
        if !govalidator.IsUUID(id) {
×
621
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
622
                return
×
623
        }
×
624

625
        constructor, err := getImageMetaFromBody(r)
×
626
        if err != nil {
×
627
                d.view.RenderError(
×
628
                        w,
×
629
                        r,
×
630
                        errors.Wrap(err, "Validating request body"),
×
631
                        http.StatusBadRequest,
×
632
                        l,
×
633
                )
×
634
                return
×
635
        }
×
636

637
        found, err := d.app.EditImage(r.Context(), id, constructor)
×
638
        if err != nil {
×
639
                if err == app.ErrModelImageUsedInAnyDeployment {
×
640
                        d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
641
                        return
×
642
                }
×
643
                d.view.RenderInternalError(w, r, err, l)
×
644
                return
×
645
        }
646

647
        if !found {
×
648
                d.view.RenderErrorNotFound(w, r, l)
×
649
                return
×
650
        }
×
651

652
        d.view.RenderSuccessPut(w)
×
653
}
654

655
func getImageMetaFromBody(r *rest.Request) (*model.ImageMeta, error) {
×
656

×
657
        var constructor *model.ImageMeta
×
658

×
659
        if err := r.DecodeJsonPayload(&constructor); err != nil {
×
660
                return nil, err
×
661
        }
×
662

663
        if err := constructor.Validate(); err != nil {
×
664
                return nil, err
×
665
        }
×
666

667
        return constructor, nil
×
668
}
669

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

678
func (d *DeploymentsApiHandlers) NewImageForTenantHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
679
        l := requestlog.GetRequestLogger(r)
1✔
680

1✔
681
        tenantID := r.PathParam("tenant")
1✔
682

1✔
683
        if tenantID == "" {
1✔
684
                rest_utils.RestErrWithLog(
×
685
                        w,
×
686
                        r,
×
687
                        l,
×
688
                        fmt.Errorf("missing tenant id in path"),
×
689
                        http.StatusBadRequest,
×
690
                )
×
691
                return
×
692
        }
×
693

694
        var ctx context.Context
1✔
695
        if tenantID != "default" {
1✔
UNCOV
696
                ident := &identity.Identity{Tenant: tenantID}
×
UNCOV
697
                ctx = identity.WithContext(r.Context(), ident)
×
698
        } else {
1✔
699
                ctx = r.Context()
1✔
700
        }
1✔
701

702
        d.newImageWithContext(ctx, w, r)
1✔
703
}
704

705
func (d *DeploymentsApiHandlers) newImageWithContext(
706
        ctx context.Context,
707
        w rest.ResponseWriter,
708
        r *rest.Request,
709
) {
1✔
710
        l := requestlog.GetRequestLogger(r)
1✔
711

1✔
712
        formReader, err := r.MultipartReader()
1✔
713
        if err != nil {
2✔
714
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
715
                return
1✔
716
        }
1✔
717

718
        // parse multipart message
719
        multipartUploadMsg, err := d.ParseMultipart(formReader)
1✔
720

1✔
721
        if err != nil {
2✔
722
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
723
                return
1✔
724
        }
1✔
725

726
        imgID, err := d.app.CreateImage(ctx, multipartUploadMsg)
1✔
727
        if err == nil {
2✔
728
                d.view.RenderSuccessPost(w, r, imgID)
1✔
729
                return
1✔
730
        }
1✔
731
        var cErr *model.ConflictError
1✔
732
        if errors.As(err, &cErr) {
2✔
733
                w.WriteHeader(http.StatusConflict)
1✔
734
                _ = cErr.WithRequestID(requestid.FromContext(ctx))
1✔
735
                err = w.WriteJson(cErr)
1✔
736
                if err != nil {
1✔
737
                        l.Error(err)
×
738
                } else {
1✔
739
                        l.Error(cErr.Error())
1✔
740
                }
1✔
741
                return
1✔
742
        }
UNCOV
743
        cause := errors.Cause(err)
×
UNCOV
744
        switch cause {
×
745
        default:
×
746
                d.view.RenderInternalError(w, r, err, l)
×
747
                return
×
748
        case app.ErrModelArtifactNotUnique:
×
749
                l.Error(err.Error())
×
750
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
×
751
                return
×
UNCOV
752
        case app.ErrModelParsingArtifactFailed:
×
UNCOV
753
                l.Error(err.Error())
×
UNCOV
754
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
×
UNCOV
755
                return
×
756
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
×
757
                d.view.RenderError(w, r, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge, l)
×
758
                return
×
759
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
760
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
761
                io.ErrUnexpectedEOF:
×
762
                l.Error(err.Error())
×
763
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
764
                return
×
765
        }
766
}
767

768
func formatArtifactUploadError(err error) error {
1✔
769
        // remove generic message
1✔
770
        errMsg := strings.TrimSuffix(err.Error(), ": "+app.ErrModelParsingArtifactFailed.Error())
1✔
771

1✔
772
        // handle specific cases
1✔
773

1✔
774
        if strings.Contains(errMsg, "invalid checksum") {
1✔
775
                return errors.New(errMsg[strings.Index(errMsg, "invalid checksum"):])
×
776
        }
×
777

778
        if strings.Contains(errMsg, "unsupported version") {
1✔
779
                return errors.New(errMsg[strings.Index(errMsg, "unsupported version"):] +
×
780
                        "; supported versions are: 1, 2")
×
781
        }
×
782

783
        return errors.New(errMsg)
1✔
784
}
785

786
// GenerateImage s the multipart Raw Data/Meta upload handler.
787
// Request should be of type "multipart/form-data". The parts are
788
// key/valyue pairs of metadata information except the last one,
789
// which must contain the file containing the raw data to be processed
790
// into an artifact.
791
func (d *DeploymentsApiHandlers) GenerateImage(w rest.ResponseWriter, r *rest.Request) {
1✔
792
        l := requestlog.GetRequestLogger(r)
1✔
793

1✔
794
        formReader, err := r.MultipartReader()
1✔
795
        if err != nil {
2✔
796
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
797
                return
1✔
798
        }
1✔
799

800
        // parse multipart message
801
        multipartMsg, err := d.ParseGenerateImageMultipart(formReader)
1✔
802
        if err != nil {
2✔
803
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
804
                return
1✔
805
        }
1✔
806

807
        tokenFields := strings.Fields(r.Header.Get("Authorization"))
1✔
808
        if len(tokenFields) == 2 && strings.EqualFold(tokenFields[0], "Bearer") {
2✔
809
                multipartMsg.Token = tokenFields[1]
1✔
810
        }
1✔
811

812
        imgID, err := d.app.GenerateImage(r.Context(), multipartMsg)
1✔
813
        cause := errors.Cause(err)
1✔
814
        switch cause {
1✔
815
        default:
1✔
816
                d.view.RenderInternalError(w, r, err, l)
1✔
817
        case nil:
1✔
818
                d.view.RenderSuccessPost(w, r, imgID)
1✔
819
        case app.ErrModelArtifactNotUnique:
1✔
820
                l.Error(err.Error())
1✔
821
                d.view.RenderError(w, r, cause, http.StatusUnprocessableEntity, l)
1✔
822
        case app.ErrModelParsingArtifactFailed:
1✔
823
                l.Error(err.Error())
1✔
824
                d.view.RenderError(w, r, formatArtifactUploadError(err), http.StatusBadRequest, l)
1✔
825
        case utils.ErrStreamTooLarge, ErrModelArtifactFileTooLarge:
1✔
826
                d.view.RenderError(w, r, ErrModelArtifactFileTooLarge, http.StatusRequestEntityTooLarge, l)
1✔
827
        case app.ErrModelMissingInputMetadata, app.ErrModelMissingInputArtifact,
828
                app.ErrModelInvalidMetadata, app.ErrModelMultipartUploadMsgMalformed,
829
                io.ErrUnexpectedEOF:
×
830
                l.Error(err.Error())
×
831
                d.view.RenderError(w, r, cause, http.StatusBadRequest, l)
×
832
        }
833
}
834

835
// ParseMultipart parses multipart/form-data message.
836
func (d *DeploymentsApiHandlers) ParseMultipart(
837
        r *multipart.Reader,
838
) (*model.MultipartUploadMsg, error) {
1✔
839
        uploadMsg := &model.MultipartUploadMsg{
1✔
840
                MetaConstructor: &model.ImageMeta{},
1✔
841
        }
1✔
842
        var size int64
1✔
843
        // Parse the multipart form sequentially. To remain backward compatible
1✔
844
        // all form names that are not part of the API are ignored.
1✔
845
        for {
2✔
846
                part, err := r.NextPart()
1✔
847
                if err != nil {
2✔
848
                        if err == io.EOF {
2✔
849
                                // The whole message has been consumed without
1✔
850
                                // the "artifact" form part.
1✔
851
                                return nil, ErrArtifactFileMissing
1✔
852
                        }
1✔
853
                        return nil, err
×
854
                }
855
                switch strings.ToLower(part.FormName()) {
1✔
856
                case "description":
1✔
857
                        // Add description to the metadata
1✔
858
                        dscr, err := io.ReadAll(part)
1✔
859
                        if err != nil {
1✔
860
                                return nil, err
×
861
                        }
×
862
                        uploadMsg.MetaConstructor.Description = string(dscr)
1✔
863

864
                case "size":
1✔
865
                        // Add size limit to the metadata
1✔
866
                        sz, err := io.ReadAll(part)
1✔
867
                        if err != nil {
1✔
868
                                return nil, err
×
869
                        }
×
870
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
871
                        if err != nil {
1✔
872
                                return nil, err
×
873
                        }
×
874
                        if size > d.config.MaxImageSize {
1✔
875
                                return nil, ErrModelArtifactFileTooLarge
×
876
                        }
×
877

878
                case "artifact_id":
1✔
879
                        // Add artifact id to the metadata (must be a valid UUID).
1✔
880
                        b, err := io.ReadAll(part)
1✔
881
                        if err != nil {
1✔
882
                                return nil, err
×
883
                        }
×
884
                        id := string(b)
1✔
885
                        if !govalidator.IsUUID(id) {
2✔
886
                                return nil, errors.New(
1✔
887
                                        "artifact_id is not a valid UUID",
1✔
888
                                )
1✔
889
                        }
1✔
890
                        uploadMsg.ArtifactID = id
1✔
891

892
                case "artifact":
1✔
893
                        // Assign the form-data payload to the artifact reader
1✔
894
                        // and return. The content is consumed elsewhere.
1✔
895
                        if size > 0 {
2✔
896
                                uploadMsg.ArtifactReader = utils.ReadExactly(part, size)
1✔
897
                        } else {
1✔
UNCOV
898
                                uploadMsg.ArtifactReader = utils.ReadAtMost(
×
UNCOV
899
                                        part,
×
UNCOV
900
                                        d.config.MaxImageSize,
×
UNCOV
901
                                )
×
UNCOV
902
                        }
×
903
                        return uploadMsg, nil
1✔
904

905
                default:
1✔
906
                        // Ignore all non-API sections.
1✔
907
                        continue
1✔
908
                }
909
        }
910
}
911

912
// ParseGenerateImageMultipart parses multipart/form-data message.
913
func (d *DeploymentsApiHandlers) ParseGenerateImageMultipart(
914
        r *multipart.Reader,
915
) (*model.MultipartGenerateImageMsg, error) {
1✔
916
        msg := &model.MultipartGenerateImageMsg{}
1✔
917
        var size int64
1✔
918

1✔
919
ParseLoop:
1✔
920
        for {
2✔
921
                part, err := r.NextPart()
1✔
922
                if err != nil {
2✔
923
                        if err == io.EOF {
2✔
924
                                break
1✔
925
                        }
926
                        return nil, err
×
927
                }
928
                switch strings.ToLower(part.FormName()) {
1✔
929
                case "args":
1✔
930
                        b, err := io.ReadAll(part)
1✔
931
                        if err != nil {
1✔
932
                                return nil, errors.Wrap(err,
×
933
                                        "failed to read form value 'args'",
×
934
                                )
×
935
                        }
×
936
                        msg.Args = string(b)
1✔
937

938
                case "description":
1✔
939
                        b, err := io.ReadAll(part)
1✔
940
                        if err != nil {
1✔
941
                                return nil, errors.Wrap(err,
×
942
                                        "failed to read form value 'description'",
×
943
                                )
×
944
                        }
×
945
                        msg.Description = string(b)
1✔
946

947
                case "device_types_compatible":
1✔
948
                        b, err := io.ReadAll(part)
1✔
949
                        if err != nil {
1✔
950
                                return nil, errors.Wrap(err,
×
951
                                        "failed to read form value 'device_types_compatible'",
×
952
                                )
×
953
                        }
×
954
                        msg.DeviceTypesCompatible = strings.Split(string(b), ",")
1✔
955

956
                case "file":
1✔
957
                        if size > 0 {
2✔
958
                                msg.FileReader = utils.ReadExactly(part, size)
1✔
959
                        } else {
2✔
960
                                msg.FileReader = utils.ReadAtMost(part, d.config.MaxGenerateDataSize)
1✔
961
                        }
1✔
962
                        break ParseLoop
1✔
963

964
                case "name":
1✔
965
                        b, err := io.ReadAll(part)
1✔
966
                        if err != nil {
1✔
967
                                return nil, errors.Wrap(err,
×
968
                                        "failed to read form value 'name'",
×
969
                                )
×
970
                        }
×
971
                        msg.Name = string(b)
1✔
972

973
                case "type":
1✔
974
                        b, err := io.ReadAll(part)
1✔
975
                        if err != nil {
1✔
976
                                return nil, errors.Wrap(err,
×
977
                                        "failed to read form value 'type'",
×
978
                                )
×
979
                        }
×
980
                        msg.Type = string(b)
1✔
981

982
                case "size":
1✔
983
                        // Add size limit to the metadata
1✔
984
                        sz, err := io.ReadAll(part)
1✔
985
                        if err != nil {
1✔
986
                                return nil, err
×
987
                        }
×
988
                        size, err = strconv.ParseInt(string(sz), 10, 64)
1✔
989
                        if err != nil {
1✔
990
                                return nil, err
×
991
                        }
×
992
                        if size > d.config.MaxGenerateDataSize {
1✔
993
                                return nil, ErrModelArtifactFileTooLarge
×
994
                        }
×
995

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

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

1005
// deployments
1006
func (d *DeploymentsApiHandlers) createDeployment(
1007
        w rest.ResponseWriter,
1008
        r *rest.Request,
1009
        ctx context.Context,
1010
        l *log.Logger,
1011
        group string,
1012
) {
1✔
1013
        constructor, err := d.getDeploymentConstructorFromBody(r, group)
1✔
1014
        if err != nil {
2✔
1015
                d.view.RenderError(
1✔
1016
                        w,
1✔
1017
                        r,
1✔
1018
                        errors.Wrap(err, "Validating request body"),
1✔
1019
                        http.StatusBadRequest,
1✔
1020
                        l,
1✔
1021
                )
1✔
1022
                return
1✔
1023
        }
1✔
1024

1025
        id, err := d.app.CreateDeployment(ctx, constructor)
1✔
1026
        switch err {
1✔
1027
        case nil:
1✔
1028
                // in case of deployment to group remove "/group/{name}" from path before creating location
1✔
1029
                // haeder
1✔
1030
                r.URL.Path = strings.TrimSuffix(r.URL.Path, "/group/"+constructor.Group)
1✔
1031
                d.view.RenderSuccessPost(w, r, id)
1✔
UNCOV
1032
        case app.ErrNoArtifact:
×
UNCOV
1033
                d.view.RenderError(w, r, err, http.StatusUnprocessableEntity, l)
×
1034
        case app.ErrNoDevices:
1✔
1035
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1036
        case app.ErrConflictingDeployment:
1✔
1037
                d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
1038
        default:
1✔
1039
                d.view.RenderInternalError(w, r, err, l)
1✔
1040
        }
1041
}
1042

1043
func (d *DeploymentsApiHandlers) PostDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1044
        ctx := r.Context()
1✔
1045
        l := requestlog.GetRequestLogger(r)
1✔
1046

1✔
1047
        d.createDeployment(w, r, ctx, l, "")
1✔
1048
}
1✔
1049

1050
func (d *DeploymentsApiHandlers) DeployToGroup(w rest.ResponseWriter, r *rest.Request) {
1✔
1051
        ctx := r.Context()
1✔
1052
        l := requestlog.GetRequestLogger(r)
1✔
1053

1✔
1054
        group := r.PathParam("name")
1✔
1055
        if len(group) < 1 {
1✔
1056
                d.view.RenderError(w, r, ErrMissingGroupName, http.StatusBadRequest, l)
×
1057
        }
×
1058
        d.createDeployment(w, r, ctx, l, group)
1✔
1059
}
1060

1061
// parseDeviceConfigurationDeploymentPathParams parses expected params
1062
// and check if the params are not empty
1063
func parseDeviceConfigurationDeploymentPathParams(r *rest.Request) (string, string, string, error) {
1✔
1064
        tenantID := r.PathParam("tenant")
1✔
1065
        deviceID := r.PathParam(ParamDeviceID)
1✔
1066
        if deviceID == "" {
1✔
1067
                return "", "", "", errors.New("device ID missing")
×
1068
        }
×
1069
        deploymentID := r.PathParam(ParamDeploymentID)
1✔
1070
        if deploymentID == "" {
1✔
1071
                return "", "", "", errors.New("deployment ID missing")
×
1072
        }
×
1073
        return tenantID, deviceID, deploymentID, nil
1✔
1074
}
1075

1076
// getConfigurationDeploymentConstructorFromBody extracts configuration
1077
// deployment constructor from the request body and validates it
1078
func getConfigurationDeploymentConstructorFromBody(r *rest.Request) (
1079
        *model.ConfigurationDeploymentConstructor, error) {
1✔
1080

1✔
1081
        var constructor *model.ConfigurationDeploymentConstructor
1✔
1082

1✔
1083
        if err := r.DecodeJsonPayload(&constructor); err != nil {
2✔
1084
                return nil, err
1✔
1085
        }
1✔
1086

1087
        if err := constructor.Validate(); err != nil {
2✔
1088
                return nil, err
1✔
1089
        }
1✔
1090

1091
        return constructor, nil
1✔
1092
}
1093

1094
// device configuration deployment handler
1095
func (d *DeploymentsApiHandlers) PostDeviceConfigurationDeployment(
1096
        w rest.ResponseWriter,
1097
        r *rest.Request,
1098
) {
1✔
1099
        l := requestlog.GetRequestLogger(r)
1✔
1100

1✔
1101
        // get path params
1✔
1102
        tenantID, deviceID, deploymentID, err := parseDeviceConfigurationDeploymentPathParams(r)
1✔
1103
        if err != nil {
1✔
1104
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
1105
                return
×
1106
        }
×
1107

1108
        // add tenant id to the context
1109
        ctx := identity.WithContext(r.Context(), &identity.Identity{Tenant: tenantID})
1✔
1110

1✔
1111
        constructor, err := getConfigurationDeploymentConstructorFromBody(r)
1✔
1112
        if err != nil {
2✔
1113
                d.view.RenderError(
1✔
1114
                        w,
1✔
1115
                        r,
1✔
1116
                        errors.Wrap(err, "Validating request body"),
1✔
1117
                        http.StatusBadRequest,
1✔
1118
                        l,
1✔
1119
                )
1✔
1120
                return
1✔
1121
        }
1✔
1122

1123
        id, err := d.app.CreateDeviceConfigurationDeployment(ctx, constructor, deviceID, deploymentID)
1✔
1124
        switch err {
1✔
1125
        default:
1✔
1126
                d.view.RenderInternalError(w, r, err, l)
1✔
1127
        case nil:
1✔
1128
                r.URL.Path = "./deployments"
1✔
1129
                d.view.RenderSuccessPost(w, r, id)
1✔
1130
        case app.ErrDuplicateDeployment:
1✔
1131
                d.view.RenderError(w, r, err, http.StatusConflict, l)
1✔
UNCOV
1132
        case app.ErrInvalidDeploymentID:
×
UNCOV
1133
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1134
        }
1135
}
1136

1137
func (d *DeploymentsApiHandlers) getDeploymentConstructorFromBody(
1138
        r *rest.Request,
1139
        group string,
1140
) (*model.DeploymentConstructor, error) {
1✔
1141
        var constructor *model.DeploymentConstructor
1✔
1142
        if err := r.DecodeJsonPayload(&constructor); err != nil {
2✔
1143
                return nil, err
1✔
1144
        }
1✔
1145

1146
        constructor.Group = group
1✔
1147

1✔
1148
        if err := constructor.ValidateNew(); err != nil {
2✔
1149
                return nil, err
1✔
1150
        }
1✔
1151

1152
        return constructor, nil
1✔
1153
}
1154

UNCOV
1155
func (d *DeploymentsApiHandlers) GetDeployment(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1156
        ctx := r.Context()
×
UNCOV
1157
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1158

×
UNCOV
1159
        id := r.PathParam("id")
×
UNCOV
1160

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

UNCOV
1166
        deployment, err := d.app.GetDeployment(ctx, id)
×
UNCOV
1167
        if err != nil {
×
1168
                d.view.RenderInternalError(w, r, err, l)
×
1169
                return
×
1170
        }
×
1171

UNCOV
1172
        if deployment == nil {
×
1173
                d.view.RenderErrorNotFound(w, r, l)
×
1174
                return
×
1175
        }
×
1176

UNCOV
1177
        d.view.RenderSuccessGet(w, deployment)
×
1178
}
1179

UNCOV
1180
func (d *DeploymentsApiHandlers) GetDeploymentStats(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1181
        ctx := r.Context()
×
UNCOV
1182
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1183

×
UNCOV
1184
        id := r.PathParam("id")
×
UNCOV
1185

×
UNCOV
1186
        if !govalidator.IsUUID(id) {
×
1187
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1188
                return
×
1189
        }
×
1190

UNCOV
1191
        stats, err := d.app.GetDeploymentStats(ctx, id)
×
UNCOV
1192
        if err != nil {
×
1193
                d.view.RenderInternalError(w, r, err, l)
×
1194
                return
×
1195
        }
×
1196

UNCOV
1197
        if stats == nil {
×
1198
                d.view.RenderErrorNotFound(w, r, l)
×
1199
                return
×
1200
        }
×
1201

UNCOV
1202
        d.view.RenderSuccessGet(w, stats)
×
1203
}
1204

1205
func (d *DeploymentsApiHandlers) GetDeploymentsStats(w rest.ResponseWriter, r *rest.Request) {
1✔
1206

1✔
1207
        ctx := r.Context()
1✔
1208
        l := requestlog.GetRequestLogger(r)
1✔
1209

1✔
1210
        ids := model.DeploymentIDs{}
1✔
1211
        if err := r.DecodeJsonPayload(&ids); err != nil {
1✔
1212
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1213
                return
×
1214
        }
×
1215

1216
        if len(ids.IDs) == 0 {
1✔
1217
                w.WriteHeader(http.StatusOK)
×
1218
                _ = w.WriteJson(struct{}{})
×
1219
                return
×
1220
        }
×
1221

1222
        if err := ids.Validate(); err != nil {
2✔
1223
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1224
                return
1✔
1225
        }
1✔
1226

1227
        stats, err := d.app.GetDeploymentsStats(ctx, ids.IDs...)
1✔
1228
        if err != nil {
2✔
1229
                if errors.Is(err, app.ErrModelDeploymentNotFound) {
2✔
1230
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
1✔
1231
                        return
1✔
1232
                }
1✔
1233
                d.view.RenderInternalError(w, r, err, l)
1✔
1234
                return
1✔
1235
        }
1236

1237
        w.WriteHeader(http.StatusOK)
1✔
1238

1✔
1239
        _ = w.WriteJson(stats)
1✔
1240
}
1241

1242
func (d *DeploymentsApiHandlers) GetDeploymentDeviceList(w rest.ResponseWriter, r *rest.Request) {
×
1243
        ctx := r.Context()
×
1244
        l := requestlog.GetRequestLogger(r)
×
1245

×
1246
        id := r.PathParam("id")
×
1247

×
1248
        if !govalidator.IsUUID(id) {
×
1249
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1250
                return
×
1251
        }
×
1252

1253
        deployment, err := d.app.GetDeployment(ctx, id)
×
1254
        if err != nil {
×
1255
                d.view.RenderInternalError(w, r, err, l)
×
1256
                return
×
1257
        }
×
1258

1259
        if deployment == nil {
×
1260
                d.view.RenderErrorNotFound(w, r, l)
×
1261
                return
×
1262
        }
×
1263

1264
        d.view.RenderSuccessGet(w, deployment.DeviceList)
×
1265
}
1266

UNCOV
1267
func (d *DeploymentsApiHandlers) AbortDeployment(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1268
        ctx := r.Context()
×
UNCOV
1269
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1270

×
UNCOV
1271
        id := r.PathParam("id")
×
UNCOV
1272

×
UNCOV
1273
        if !govalidator.IsUUID(id) {
×
1274
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1275
                return
×
1276
        }
×
1277

1278
        // receive request body
UNCOV
1279
        var status struct {
×
UNCOV
1280
                Status model.DeviceDeploymentStatus
×
UNCOV
1281
        }
×
UNCOV
1282

×
UNCOV
1283
        err := r.DecodeJsonPayload(&status)
×
UNCOV
1284
        if err != nil {
×
1285
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1286
                return
×
1287
        }
×
1288
        // "aborted" is the only supported status
UNCOV
1289
        if status.Status != model.DeviceDeploymentStatusAborted {
×
1290
                d.view.RenderError(w, r, ErrUnexpectedDeploymentStatus, http.StatusBadRequest, l)
×
1291
        }
×
1292

UNCOV
1293
        l.Infof("Abort deployment: %s", id)
×
UNCOV
1294

×
UNCOV
1295
        // Check if deployment is finished
×
UNCOV
1296
        isDeploymentFinished, err := d.app.IsDeploymentFinished(ctx, id)
×
UNCOV
1297
        if err != nil {
×
1298
                d.view.RenderInternalError(w, r, err, l)
×
1299
                return
×
1300
        }
×
UNCOV
1301
        if isDeploymentFinished {
×
UNCOV
1302
                d.view.RenderError(w, r, ErrDeploymentAlreadyFinished, http.StatusUnprocessableEntity, l)
×
UNCOV
1303
                return
×
UNCOV
1304
        }
×
1305

1306
        // Abort deployments for devices and update deployment stats
UNCOV
1307
        if err := d.app.AbortDeployment(ctx, id); err != nil {
×
1308
                d.view.RenderInternalError(w, r, err, l)
×
1309
        }
×
1310

UNCOV
1311
        d.view.RenderEmptySuccessResponse(w)
×
1312
}
1313

1314
func (d *DeploymentsApiHandlers) GetDeploymentForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1315
        var (
1✔
1316
                installed *model.InstalledDeviceDeployment
1✔
1317
                ctx       = r.Context()
1✔
1318
                l         = requestlog.GetRequestLogger(r)
1✔
1319
                idata     = identity.FromContext(ctx)
1✔
1320
        )
1✔
1321
        if idata == nil {
2✔
1322
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
1✔
1323
                return
1✔
1324
        }
1✔
1325

1326
        q := r.URL.Query()
1✔
1327
        defer func() {
2✔
1328
                var reEncode bool = false
1✔
1329
                if name := q.Get(ParamArtifactName); name != "" {
2✔
1330
                        q.Set(ParamArtifactName, Redacted)
1✔
1331
                        reEncode = true
1✔
1332
                }
1✔
1333
                if typ := q.Get(ParamDeviceType); typ != "" {
2✔
1334
                        q.Set(ParamDeviceType, Redacted)
1✔
1335
                        reEncode = true
1✔
1336
                }
1✔
1337
                if reEncode {
2✔
1338
                        r.URL.RawQuery = q.Encode()
1✔
1339
                }
1✔
1340
        }()
1341
        if strings.EqualFold(r.Method, http.MethodPost) {
2✔
1342
                // POST
1✔
1343
                installed = new(model.InstalledDeviceDeployment)
1✔
1344
                if err := r.DecodeJsonPayload(&installed); err != nil {
2✔
1345
                        d.view.RenderError(w, r,
1✔
1346
                                errors.Wrap(err, "invalid schema"),
1✔
1347
                                http.StatusBadRequest, l)
1✔
1348
                        return
1✔
1349
                }
1✔
1350
        } else {
1✔
1351
                // GET or HEAD
1✔
1352
                installed = &model.InstalledDeviceDeployment{
1✔
1353
                        ArtifactName: q.Get(ParamArtifactName),
1✔
1354
                        DeviceType:   q.Get(ParamDeviceType),
1✔
1355
                }
1✔
1356
        }
1✔
1357

1358
        if err := installed.Validate(); err != nil {
2✔
1359
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1360
                return
1✔
1361
        }
1✔
1362

1363
        request := &model.DeploymentNextRequest{
1✔
1364
                DeviceProvides: installed,
1✔
1365
        }
1✔
1366

1✔
1367
        d.getDeploymentForDevice(w, r, idata, request)
1✔
1368
}
1369

1370
func (d *DeploymentsApiHandlers) getDeploymentForDevice(
1371
        w rest.ResponseWriter,
1372
        r *rest.Request,
1373
        idata *identity.Identity,
1374
        request *model.DeploymentNextRequest,
1375
) {
1✔
1376
        ctx := r.Context()
1✔
1377
        l := requestlog.GetRequestLogger(r)
1✔
1378

1✔
1379
        deployment, err := d.app.GetDeploymentForDeviceWithCurrent(ctx, idata.Subject, request)
1✔
1380
        if err != nil {
2✔
1381
                if err == app.ErrConflictingRequestData {
1✔
UNCOV
1382
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
1383
                } else {
1✔
1384
                        d.view.RenderInternalError(w, r, err, l)
1✔
1385
                }
1✔
1386
                return
1✔
1387
        }
1388

1389
        if deployment == nil {
2✔
1390
                d.view.RenderNoUpdateForDevice(w)
1✔
1391
                return
1✔
1392
        } else if deployment.Type == model.DeploymentTypeConfiguration {
3✔
1393
                // Generate pre-signed URL
1✔
1394
                var hostName string = d.config.PresignHostname
1✔
1395
                if hostName == "" {
2✔
1396
                        if hostName = r.Header.Get(hdrForwardedHost); hostName == "" {
2✔
1397
                                d.view.RenderInternalError(w, r,
1✔
1398
                                        errors.New("presign.hostname not configured; "+
1✔
1399
                                                "unable to generate download link "+
1✔
1400
                                                " for configuration deployment"), l)
1✔
1401
                                return
1✔
1402
                        }
1✔
1403
                }
1404
                req, _ := http.NewRequest(
1✔
1405
                        http.MethodGet,
1✔
1406
                        FMTConfigURL(
1✔
1407
                                d.config.PresignScheme, hostName,
1✔
1408
                                deployment.ID, request.DeviceProvides.DeviceType,
1✔
1409
                                idata.Subject,
1✔
1410
                        ),
1✔
1411
                        nil,
1✔
1412
                )
1✔
1413
                if idata.Tenant != "" {
2✔
1414
                        q := req.URL.Query()
1✔
1415
                        q.Set(model.ParamTenantID, idata.Tenant)
1✔
1416
                        req.URL.RawQuery = q.Encode()
1✔
1417
                }
1✔
1418
                sig := model.NewRequestSignature(req, d.config.PresignSecret)
1✔
1419
                expireTS := time.Now().Add(d.config.PresignExpire)
1✔
1420
                sig.SetExpire(expireTS)
1✔
1421
                deployment.Artifact.Source = model.Link{
1✔
1422
                        Uri:    sig.PresignURL(),
1✔
1423
                        Expire: expireTS,
1✔
1424
                }
1✔
1425
        }
1426

1427
        d.view.RenderSuccessGet(w, deployment)
1✔
1428
}
1429

1430
func (d *DeploymentsApiHandlers) PutDeploymentStatusForDevice(
1431
        w rest.ResponseWriter,
1432
        r *rest.Request,
UNCOV
1433
) {
×
UNCOV
1434
        ctx := r.Context()
×
UNCOV
1435
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1436

×
UNCOV
1437
        did := r.PathParam("id")
×
UNCOV
1438

×
UNCOV
1439
        idata := identity.FromContext(ctx)
×
UNCOV
1440
        if idata == nil {
×
1441
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1442
                return
×
1443
        }
×
1444

1445
        // receive request body
UNCOV
1446
        var report model.StatusReport
×
UNCOV
1447

×
UNCOV
1448
        err := r.DecodeJsonPayload(&report)
×
UNCOV
1449
        if err != nil {
×
UNCOV
1450
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1451
                return
×
UNCOV
1452
        }
×
1453

UNCOV
1454
        l.Infof("status: %+v", report)
×
UNCOV
1455
        if err := d.app.UpdateDeviceDeploymentStatus(ctx, did,
×
UNCOV
1456
                idata.Subject, model.DeviceDeploymentState{
×
UNCOV
1457
                        Status:   report.Status,
×
UNCOV
1458
                        SubState: report.SubState,
×
UNCOV
1459
                }); err != nil {
×
UNCOV
1460

×
UNCOV
1461
                if err == app.ErrDeploymentAborted || err == app.ErrDeviceDecommissioned {
×
1462
                        d.view.RenderError(w, r, err, http.StatusConflict, l)
×
UNCOV
1463
                } else if err == app.ErrStorageNotFound {
×
UNCOV
1464
                        d.view.RenderErrorNotFound(w, r, l)
×
UNCOV
1465
                } else {
×
1466
                        d.view.RenderInternalError(w, r, err, l)
×
1467
                }
×
UNCOV
1468
                return
×
1469
        }
1470

UNCOV
1471
        d.view.RenderEmptySuccessResponse(w)
×
1472
}
1473

1474
func (d *DeploymentsApiHandlers) GetDeviceStatusesForDeployment(
1475
        w rest.ResponseWriter,
1476
        r *rest.Request,
UNCOV
1477
) {
×
UNCOV
1478
        ctx := r.Context()
×
UNCOV
1479
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1480

×
UNCOV
1481
        did := r.PathParam("id")
×
UNCOV
1482

×
UNCOV
1483
        if !govalidator.IsUUID(did) {
×
1484
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1485
                return
×
1486
        }
×
1487

UNCOV
1488
        statuses, err := d.app.GetDeviceStatusesForDeployment(ctx, did)
×
UNCOV
1489
        if err != nil {
×
1490
                switch err {
×
1491
                case app.ErrModelDeploymentNotFound:
×
1492
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1493
                        return
×
1494
                default:
×
1495
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1496
                        return
×
1497
                }
1498
        }
1499

UNCOV
1500
        d.view.RenderSuccessGet(w, statuses)
×
1501
}
1502

1503
func (d *DeploymentsApiHandlers) GetDevicesListForDeployment(
1504
        w rest.ResponseWriter,
1505
        r *rest.Request,
UNCOV
1506
) {
×
UNCOV
1507
        ctx := r.Context()
×
UNCOV
1508
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1509

×
UNCOV
1510
        did := r.PathParam("id")
×
UNCOV
1511

×
UNCOV
1512
        if !govalidator.IsUUID(did) {
×
1513
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
×
1514
                return
×
1515
        }
×
1516

UNCOV
1517
        page, perPage, err := rest_utils.ParsePagination(r)
×
UNCOV
1518
        if err != nil {
×
1519
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1520
                return
×
1521
        }
×
1522

UNCOV
1523
        lq := store.ListQuery{
×
UNCOV
1524
                Skip:         int((page - 1) * perPage),
×
UNCOV
1525
                Limit:        int(perPage),
×
UNCOV
1526
                DeploymentID: did,
×
UNCOV
1527
        }
×
UNCOV
1528
        if status := r.URL.Query().Get("status"); status != "" {
×
1529
                lq.Status = &status
×
1530
        }
×
UNCOV
1531
        if err = lq.Validate(); err != nil {
×
1532
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1533
                return
×
1534
        }
×
1535

UNCOV
1536
        statuses, totalCount, err := d.app.GetDevicesListForDeployment(ctx, lq)
×
UNCOV
1537
        if err != nil {
×
1538
                switch err {
×
1539
                case app.ErrModelDeploymentNotFound:
×
1540
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1541
                        return
×
1542
                default:
×
1543
                        d.view.RenderInternalError(w, r, ErrInternal, l)
×
1544
                        return
×
1545
                }
1546
        }
1547

UNCOV
1548
        hasNext := totalCount > int(page*perPage)
×
UNCOV
1549
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
×
UNCOV
1550
        for _, l := range links {
×
UNCOV
1551
                w.Header().Add(hdrLink, l)
×
UNCOV
1552
        }
×
UNCOV
1553
        w.Header().Add(hdrTotalCount, strconv.Itoa(totalCount))
×
UNCOV
1554
        d.view.RenderSuccessGet(w, statuses)
×
1555
}
1556

1557
func ParseLookupQuery(vals url.Values) (model.Query, error) {
1✔
1558
        query := model.Query{}
1✔
1559

1✔
1560
        createdBefore := vals.Get("created_before")
1✔
1561
        if createdBefore != "" {
2✔
1562
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
2✔
1563
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1564
                } else {
1✔
UNCOV
1565
                        query.CreatedBefore = &createdBeforeTime
×
UNCOV
1566
                }
×
1567
        }
1568

1569
        createdAfter := vals.Get("created_after")
1✔
1570
        if createdAfter != "" {
1✔
UNCOV
1571
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1572
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
UNCOV
1573
                } else {
×
UNCOV
1574
                        query.CreatedAfter = &createdAfterTime
×
UNCOV
1575
                }
×
1576
        }
1577

1578
        switch strings.ToLower(vals.Get("sort")) {
1✔
1579
        case model.SortDirectionAscending:
1✔
1580
                query.Sort = model.SortDirectionAscending
1✔
1581
        case "", model.SortDirectionDescending:
1✔
1582
                query.Sort = model.SortDirectionDescending
1✔
1583
        default:
×
1584
                return query, ErrInvalidSortDirection
×
1585
        }
1586

1587
        status := vals.Get("status")
1✔
1588
        switch status {
1✔
1589
        case "inprogress":
×
1590
                query.Status = model.StatusQueryInProgress
×
1591
        case "finished":
×
1592
                query.Status = model.StatusQueryFinished
×
1593
        case "pending":
×
1594
                query.Status = model.StatusQueryPending
×
1595
        case "aborted":
×
1596
                query.Status = model.StatusQueryAborted
×
1597
        case "":
1✔
1598
                query.Status = model.StatusQueryAny
1✔
1599
        default:
×
1600
                return query, errors.Errorf("unknown status %s", status)
×
1601

1602
        }
1603

1604
        dType := vals.Get("type")
1✔
1605
        if dType == "" {
2✔
1606
                return query, nil
1✔
1607
        }
1✔
1608
        deploymentType := model.DeploymentType(dType)
×
1609
        if deploymentType == model.DeploymentTypeSoftware ||
×
1610
                deploymentType == model.DeploymentTypeConfiguration {
×
1611
                query.Type = deploymentType
×
1612
        } else {
×
1613
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1614
        }
×
1615

1616
        return query, nil
×
1617
}
1618

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

1625
        search := vals.Get("search")
1✔
1626
        if search != "" {
1✔
1627
                query.SearchText = search
×
1628
        }
×
1629

1630
        return query, nil
1✔
1631
}
1632

1633
func ParseDeploymentLookupQueryV2(vals url.Values) (model.Query, error) {
1✔
1634
        query, err := ParseLookupQuery(vals)
1✔
1635
        if err != nil {
1✔
1636
                return query, err
×
1637
        }
×
1638

1639
        query.Names = vals["name"]
1✔
1640
        query.IDs = vals["id"]
1✔
1641

1✔
1642
        return query, nil
1✔
1643
}
1644

1645
func parseEpochToTimestamp(epoch string) (time.Time, error) {
1✔
1646
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
2✔
1647
                return time.Time{}, errors.New("invalid timestamp: " + epoch)
1✔
1648
        } else {
1✔
UNCOV
1649
                return time.Unix(epochInt64, 0).UTC(), nil
×
UNCOV
1650
        }
×
1651
}
1652

1653
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
1✔
1654
        ctx := r.Context()
1✔
1655
        l := requestlog.GetRequestLogger(r)
1✔
1656
        q := r.URL.Query()
1✔
1657
        defer func() {
2✔
1658
                if search := q.Get("search"); search != "" {
1✔
1659
                        q.Set("search", Redacted)
×
1660
                        r.URL.RawQuery = q.Encode()
×
1661
                }
×
1662
        }()
1663

1664
        query, err := ParseDeploymentLookupQueryV1(q)
1✔
1665
        if err != nil {
2✔
1666
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1667
                return
1✔
1668
        }
1✔
1669

1670
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1671
        if err != nil {
2✔
1672
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1673
                return
1✔
1674
        }
1✔
1675
        query.Skip = int((page - 1) * perPage)
1✔
1676
        query.Limit = int(perPage + 1)
1✔
1677

1✔
1678
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
1✔
1679
        if err != nil {
2✔
1680
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1681
                return
1✔
1682
        }
1✔
1683
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
1✔
1684

1✔
1685
        len := len(deps)
1✔
1686
        hasNext := false
1✔
1687
        if uint64(len) > perPage {
1✔
1688
                hasNext = true
×
1689
                len = int(perPage)
×
1690
        }
×
1691

1692
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1693
        for _, l := range links {
2✔
1694
                w.Header().Add(hdrLink, l)
1✔
1695
        }
1✔
1696

1697
        d.view.RenderSuccessGet(w, deps[:len])
1✔
1698
}
1699

1700
func (d *DeploymentsApiHandlers) LookupDeploymentV2(w rest.ResponseWriter, r *rest.Request) {
1✔
1701
        ctx := r.Context()
1✔
1702
        l := requestlog.GetRequestLogger(r)
1✔
1703
        q := r.URL.Query()
1✔
1704
        defer func() {
2✔
1705
                if q.Has("name") {
1✔
UNCOV
1706
                        q["name"] = []string{Redacted}
×
UNCOV
1707
                        r.URL.RawQuery = q.Encode()
×
UNCOV
1708
                }
×
1709
        }()
1710

1711
        query, err := ParseDeploymentLookupQueryV2(q)
1✔
1712
        if err != nil {
1✔
1713
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1714
                return
×
1715
        }
×
1716

1717
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1718
        if err != nil {
1✔
UNCOV
1719
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
UNCOV
1720
                return
×
UNCOV
1721
        }
×
1722
        query.Skip = int((page - 1) * perPage)
1✔
1723
        query.Limit = int(perPage + 1)
1✔
1724

1✔
1725
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
1✔
1726
        if err != nil {
1✔
1727
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1728
                return
×
1729
        }
×
1730
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
1✔
1731

1✔
1732
        len := len(deps)
1✔
1733
        hasNext := false
1✔
1734
        if uint64(len) > perPage {
1✔
UNCOV
1735
                hasNext = true
×
UNCOV
1736
                len = int(perPage)
×
UNCOV
1737
        }
×
1738

1739
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1740
        for _, l := range links {
2✔
1741
                w.Header().Add(hdrLink, l)
1✔
1742
        }
1✔
1743

1744
        d.view.RenderSuccessGet(w, deps[:len])
1✔
1745
}
1746

UNCOV
1747
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1748
        ctx := r.Context()
×
UNCOV
1749
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1750

×
UNCOV
1751
        did := r.PathParam("id")
×
UNCOV
1752

×
UNCOV
1753
        idata := identity.FromContext(ctx)
×
UNCOV
1754
        if idata == nil {
×
1755
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1756
                return
×
1757
        }
×
1758

1759
        // reuse DeploymentLog, device and deployment IDs are ignored when
1760
        // (un-)marshaling DeploymentLog to/from JSON
UNCOV
1761
        var log model.DeploymentLog
×
UNCOV
1762

×
UNCOV
1763
        err := r.DecodeJsonPayload(&log)
×
UNCOV
1764
        if err != nil {
×
1765
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1766
                return
×
1767
        }
×
1768

UNCOV
1769
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
×
UNCOV
1770
                did, log.Messages); err != nil {
×
1771

×
1772
                if err == app.ErrModelDeploymentNotFound {
×
1773
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1774
                } else {
×
1775
                        d.view.RenderInternalError(w, r, err, l)
×
1776
                }
×
1777
                return
×
1778
        }
1779

UNCOV
1780
        d.view.RenderEmptySuccessResponse(w)
×
1781
}
1782

UNCOV
1783
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1784
        ctx := r.Context()
×
UNCOV
1785
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1786

×
UNCOV
1787
        did := r.PathParam("id")
×
UNCOV
1788
        devid := r.PathParam("devid")
×
UNCOV
1789

×
UNCOV
1790
        depl, err := d.app.GetDeviceDeploymentLog(ctx, devid, did)
×
UNCOV
1791

×
UNCOV
1792
        if err != nil {
×
1793
                d.view.RenderInternalError(w, r, err, l)
×
1794
                return
×
1795
        }
×
1796

UNCOV
1797
        if depl == nil {
×
1798
                d.view.RenderErrorNotFound(w, r, l)
×
1799
                return
×
1800
        }
×
1801

UNCOV
1802
        d.view.RenderDeploymentLog(w, *depl)
×
1803
}
1804

1805
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
1✔
1806
        ctx := r.Context()
1✔
1807
        l := requestlog.GetRequestLogger(r)
1✔
1808

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

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

1820
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1821
        r *rest.Request) {
1✔
1822
        ctx := r.Context()
1✔
1823
        l := requestlog.GetRequestLogger(r)
1✔
1824

1✔
1825
        id := r.PathParam("id")
1✔
1826
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
1✔
1827

1✔
1828
        switch err {
1✔
1829
        case nil, app.ErrStorageNotFound:
1✔
1830
                d.view.RenderEmptySuccessResponse(w)
1✔
1831
        default:
1✔
1832
                d.view.RenderInternalError(w, r, err, l)
1✔
1833
        }
1834
}
1835

1836
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
1✔
1837
        ctx := r.Context()
1✔
1838
        d.listDeviceDeployments(ctx, w, r, true)
1✔
1839
}
1✔
1840

1841
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1842
        r *rest.Request) {
1✔
1843
        ctx := r.Context()
1✔
1844
        tenantID := r.PathParam("tenant")
1✔
1845
        if tenantID != "" {
2✔
1846
                ctx = identity.WithContext(r.Context(), &identity.Identity{
1✔
1847
                        Tenant:   tenantID,
1✔
1848
                        IsDevice: true,
1✔
1849
                })
1✔
1850
        }
1✔
1851
        d.listDeviceDeployments(ctx, w, r, true)
1✔
1852
}
1853

1854
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(w rest.ResponseWriter,
1855
        r *rest.Request) {
1✔
1856
        ctx := r.Context()
1✔
1857
        tenantID := r.PathParam("tenant")
1✔
1858
        if tenantID != "" {
2✔
1859
                ctx = identity.WithContext(r.Context(), &identity.Identity{
1✔
1860
                        Tenant:   tenantID,
1✔
1861
                        IsDevice: true,
1✔
1862
                })
1✔
1863
        }
1✔
1864
        d.listDeviceDeployments(ctx, w, r, false)
1✔
1865
}
1866

1867
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1868
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
1✔
1869
        l := requestlog.GetRequestLogger(r)
1✔
1870

1✔
1871
        did := ""
1✔
1872
        var IDs []string
1✔
1873
        if byDeviceID {
2✔
1874
                did = r.PathParam("id")
1✔
1875
        } else {
2✔
1876
                values := r.URL.Query()
1✔
1877
                if values.Has("id") && len(values["id"]) > 0 {
2✔
1878
                        IDs = values["id"]
1✔
1879
                } else {
2✔
1880
                        d.view.RenderError(w, r, ErrEmptyID, http.StatusBadRequest, l)
1✔
1881
                        return
1✔
1882
                }
1✔
1883
        }
1884

1885
        page, perPage, err := rest_utils.ParsePagination(r)
1✔
1886
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
2✔
1887
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
1✔
1888
        }
1✔
1889
        if err != nil {
2✔
1890
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1891
                return
1✔
1892
        }
1✔
1893

1894
        lq := store.ListQueryDeviceDeployments{
1✔
1895
                Skip:     int((page - 1) * perPage),
1✔
1896
                Limit:    int(perPage),
1✔
1897
                DeviceID: did,
1✔
1898
                IDs:      IDs,
1✔
1899
        }
1✔
1900
        if status := r.URL.Query().Get("status"); status != "" {
2✔
1901
                lq.Status = &status
1✔
1902
        }
1✔
1903
        if err = lq.Validate(); err != nil {
2✔
1904
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1905
                return
1✔
1906
        }
1✔
1907

1908
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
1✔
1909
        if err != nil {
2✔
1910
                d.view.RenderInternalError(w, r, err, l)
1✔
1911
                return
1✔
1912
        }
1✔
1913
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
1✔
1914

1✔
1915
        hasNext := totalCount > lq.Skip+len(deps)
1✔
1916
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
1✔
1917
        for _, l := range links {
2✔
1918
                w.Header().Add(hdrLink, l)
1✔
1919
        }
1✔
1920

1921
        d.view.RenderSuccessGet(w, deps)
1✔
1922
}
1923

1924
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
UNCOV
1925
        r *rest.Request) {
×
UNCOV
1926
        ctx := r.Context()
×
UNCOV
1927
        tenantID := r.PathParam("tenantID")
×
UNCOV
1928
        if tenantID != "" {
×
1929
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1930
                        Tenant:   tenantID,
×
1931
                        IsDevice: true,
×
1932
                })
×
1933
        }
×
1934

UNCOV
1935
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1936

×
UNCOV
1937
        id := r.PathParam("id")
×
UNCOV
1938

×
UNCOV
1939
        // Decommission deployments for devices and update deployment stats
×
UNCOV
1940
        err := d.app.DecommissionDevice(ctx, id)
×
UNCOV
1941

×
UNCOV
1942
        switch err {
×
UNCOV
1943
        case nil, app.ErrStorageNotFound:
×
UNCOV
1944
                d.view.RenderEmptySuccessResponse(w)
×
1945
        default:
×
1946
                d.view.RenderInternalError(w, r, err, l)
×
1947

1948
        }
1949
}
1950

1951
// tenants
1952

UNCOV
1953
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
×
UNCOV
1954
        ctx := r.Context()
×
UNCOV
1955
        l := requestlog.GetRequestLogger(r)
×
UNCOV
1956

×
UNCOV
1957
        defer r.Body.Close()
×
UNCOV
1958

×
UNCOV
1959
        tenant, err := model.ParseNewTenantReq(r.Body)
×
UNCOV
1960
        if err != nil {
×
UNCOV
1961
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
×
UNCOV
1962
                return
×
UNCOV
1963
        }
×
1964

UNCOV
1965
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
×
UNCOV
1966
        if err != nil {
×
1967
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1968
                return
×
1969
        }
×
1970

UNCOV
1971
        w.WriteHeader(http.StatusCreated)
×
1972
}
1973

1974
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1975
        w rest.ResponseWriter,
1976
        r *rest.Request,
1977
) {
1✔
1978
        tenantID := r.PathParam("tenant")
1✔
1979
        if tenantID == "" {
2✔
1980
                l := requestlog.GetRequestLogger(r)
1✔
1981
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1982
                return
1✔
1983
        }
1✔
1984

1985
        r.Request = r.WithContext(identity.WithContext(
1✔
1986
                r.Context(),
1✔
1987
                &identity.Identity{Tenant: tenantID},
1✔
1988
        ))
1✔
1989
        d.LookupDeployment(w, r)
1✔
1990
}
1991

1992
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1993
        w rest.ResponseWriter,
1994
        r *rest.Request,
1995
) {
1✔
1996
        l := requestlog.GetRequestLogger(r)
1✔
1997

1✔
1998
        tenantID := r.PathParam("tenant")
1✔
1999

1✔
2000
        ctx := identity.WithContext(
1✔
2001
                r.Context(),
1✔
2002
                &identity.Identity{Tenant: tenantID},
1✔
2003
        )
1✔
2004

1✔
2005
        settings, err := d.app.GetStorageSettings(ctx)
1✔
2006
        if err != nil {
2✔
2007
                rest_utils.RestErrWithLogInternal(w, r, l, err)
1✔
2008
                return
1✔
2009
        }
1✔
2010

2011
        d.view.RenderSuccessGet(w, settings)
1✔
2012
}
2013

2014
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
2015
        w rest.ResponseWriter,
2016
        r *rest.Request,
2017
) {
1✔
2018
        l := requestlog.GetRequestLogger(r)
1✔
2019

1✔
2020
        defer r.Body.Close()
1✔
2021

1✔
2022
        tenantID := r.PathParam("tenant")
1✔
2023

1✔
2024
        ctx := identity.WithContext(
1✔
2025
                r.Context(),
1✔
2026
                &identity.Identity{Tenant: tenantID},
1✔
2027
        )
1✔
2028

1✔
2029
        settings, err := model.ParseStorageSettingsRequest(r.Body)
1✔
2030
        if err != nil {
2✔
2031
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
2032
                return
1✔
2033
        }
1✔
2034

2035
        err = d.app.SetStorageSettings(ctx, settings)
1✔
2036
        if err != nil {
2✔
2037
                rest_utils.RestErrWithLogInternal(w, r, l, err)
1✔
2038
                return
1✔
2039
        }
1✔
2040

2041
        w.WriteHeader(http.StatusNoContent)
1✔
2042
}
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