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

mendersoftware / deployments / 1397567924

01 Aug 2024 07:00PM UTC coverage: 79.694%. Remained the same
1397567924

Pull #1033

gitlab-ci

web-flow
chore: bump the golang-dependencies group with 5 updates

Bumps the golang-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `1.12.0` | `1.13.0` |
| [github.com/Azure/azure-sdk-for-go/sdk/storage/azblob](https://github.com/Azure/azure-sdk-for-go) | `1.3.2` | `1.4.0` |
| [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) | `1.30.1` | `1.30.3` |
| [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) | `1.27.23` | `1.27.27` |
| [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) | `1.57.1` | `1.58.2` |


Updates `github.com/Azure/azure-sdk-for-go/sdk/azcore` from 1.12.0 to 1.13.0
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.12.0...sdk/azcore/v1.13.0)

Updates `github.com/Azure/azure-sdk-for-go/sdk/storage/azblob` from 1.3.2 to 1.4.0
- [Release notes](https://github.com/Azure/azure-sdk-for-go/releases)
- [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/release.md)
- [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/storage/azblob/v1.3.2...sdk/azcore/v1.4.0)

Updates `github.com/aws/aws-sdk-go-v2` from 1.30.1 to 1.30.3
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.30.1...v1.30.3)

Updates `github.com/aws/aws-sdk-go-v2/config` from 1.27.23 to 1.27.27
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.23...config/v1.27.27)

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.57.1 to 1.58.2
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https:/... (continued)
Pull Request #1033: chore: bump the golang-dependencies group with 5 updates

8124 of 10194 relevant lines covered (79.69%)

34.59 hits per line

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

76.75
/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/go-lib-micro/config"
34
        "github.com/mendersoftware/go-lib-micro/identity"
35
        "github.com/mendersoftware/go-lib-micro/log"
36
        "github.com/mendersoftware/go-lib-micro/requestid"
37
        "github.com/mendersoftware/go-lib-micro/requestlog"
38
        "github.com/mendersoftware/go-lib-micro/rest_utils"
39

40
        "github.com/mendersoftware/deployments/app"
41
        dconfig "github.com/mendersoftware/deployments/config"
42
        "github.com/mendersoftware/deployments/model"
43
        "github.com/mendersoftware/deployments/store"
44
        "github.com/mendersoftware/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
        hdrForwardedHost = "X-Forwarded-Host"
69
)
70

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

89
const Redacted = "REDACTED"
90

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

32✔
278
        q := r.URL.Query()
32✔
279

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

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

314
        return filter
32✔
315
}
316

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

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

3✔
325
        name := r.PathParam("name")
3✔
326

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

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

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

346
// images
347

348
func (d *DeploymentsApiHandlers) GetImage(w rest.ResponseWriter, r *rest.Request) {
1✔
349
        l := requestlog.GetRequestLogger(r)
1✔
350

1✔
351
        id := r.PathParam("id")
1✔
352

1✔
353
        if !govalidator.IsUUID(id) {
2✔
354
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
355
                return
1✔
356
        }
1✔
357

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

364
        if image == nil {
2✔
365
                d.view.RenderErrorNotFound(w, r, l)
1✔
366
                return
1✔
367
        }
1✔
368

369
        d.view.RenderSuccessGet(w, image)
1✔
370
}
371

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

5✔
375
        defer redactReleaseName(r)
5✔
376
        filter := getReleaseOrImageFilter(r, listReleasesV1, false)
5✔
377

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

384
        d.view.RenderSuccessGet(w, list)
4✔
385
}
386

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

4✔
390
        defer redactReleaseName(r)
4✔
391
        filter := getReleaseOrImageFilter(r, listReleasesV1, true)
4✔
392

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

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

3✔
406
        d.view.RenderSuccessGet(w, list)
3✔
407
}
408

409
func (d *DeploymentsApiHandlers) DownloadLink(w rest.ResponseWriter, r *rest.Request) {
1✔
410
        l := requestlog.GetRequestLogger(r)
1✔
411

1✔
412
        id := r.PathParam("id")
1✔
413

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

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

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

431
        d.view.RenderSuccessGet(w, link)
1✔
432
}
433

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

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

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

453
        d.view.RenderSuccessGet(w, link)
2✔
454
}
455

456
const maxMetadataSize = 2048
457

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

4✔
462
        artifactID := r.PathParam(ParamID)
4✔
463

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

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

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

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

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

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

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

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

589
func (d *DeploymentsApiHandlers) DeleteImage(w rest.ResponseWriter, r *rest.Request) {
1✔
590
        l := requestlog.GetRequestLogger(r)
1✔
591

1✔
592
        id := r.PathParam("id")
1✔
593

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

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

611
        d.view.RenderSuccessDelete(w)
1✔
612
}
613

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

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

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

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

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

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

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

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

×
656
        var constructor *model.ImageMeta
×
657

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

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

666
        return constructor, nil
×
667
}
668

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

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

7✔
680
        tenantID := r.PathParam("tenant")
7✔
681

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

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

701
        d.newImageWithContext(ctx, w, r)
7✔
702
}
703

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

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

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

9✔
720
        if err != nil {
14✔
721
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
5✔
722
                return
5✔
723
        }
5✔
724

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

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

2✔
771
        // handle specific cases
2✔
772

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

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

782
        return errors.New(errMsg)
2✔
783
}
784

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

9✔
1046
        d.createDeployment(w, r, ctx, l, "")
9✔
1047
}
9✔
1048

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

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

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

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

7✔
1080
        var constructor *model.ConfigurationDeploymentConstructor
7✔
1081

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

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

1090
        return constructor, nil
5✔
1091
}
1092

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

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

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

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

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

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

1145
        constructor.Group = group
12✔
1146

12✔
1147
        if err := constructor.ValidateNew(); err != nil {
16✔
1148
                return nil, err
4✔
1149
        }
4✔
1150

1151
        return constructor, nil
9✔
1152
}
1153

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

1✔
1158
        id := r.PathParam("id")
1✔
1159

1✔
1160
        if !govalidator.IsUUID(id) {
2✔
1161
                d.view.RenderError(w, r, ErrIDNotUUID, http.StatusBadRequest, l)
1✔
1162
                return
1✔
1163
        }
1✔
1164

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

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

1176
        d.view.RenderSuccessGet(w, deployment)
1✔
1177
}
1178

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

1✔
1183
        id := r.PathParam("id")
1✔
1184

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

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

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

1201
        d.view.RenderSuccessGet(w, stats)
1✔
1202
}
1203

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

4✔
1206
        ctx := r.Context()
4✔
1207
        l := requestlog.GetRequestLogger(r)
4✔
1208

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1270
        id := r.PathParam("id")
1✔
1271

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

1277
        // receive request body
1278
        var status struct {
1✔
1279
                Status model.DeviceDeploymentStatus
1✔
1280
        }
1✔
1281

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

1292
        l.Infof("Abort deployment: %s", id)
1✔
1293

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

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

1310
        d.view.RenderEmptySuccessResponse(w)
1✔
1311
}
1312

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

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

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

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

9✔
1366
        d.getDeploymentForDevice(w, r, idata, request)
9✔
1367
}
1368

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

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

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

1426
        d.view.RenderSuccessGet(w, deployment)
6✔
1427
}
1428

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

1✔
1436
        did := r.PathParam("id")
1✔
1437

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

1444
        // receive request body
1445
        var report model.StatusReport
1✔
1446

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

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

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

1470
        d.view.RenderEmptySuccessResponse(w)
1✔
1471
}
1472

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

1✔
1480
        did := r.PathParam("id")
1✔
1481

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

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

1499
        d.view.RenderSuccessGet(w, statuses)
1✔
1500
}
1501

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

1✔
1509
        did := r.PathParam("id")
1✔
1510

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

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

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

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

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

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

9✔
1559
        search := vals.Get("search")
9✔
1560
        if search != "" {
9✔
1561
                query.SearchText = search
×
1562
        }
×
1563

1564
        createdBefore := vals.Get("created_before")
9✔
1565
        if createdBefore != "" {
10✔
1566
                if createdBeforeTime, err := parseEpochToTimestamp(createdBefore); err != nil {
2✔
1567
                        return query, errors.Wrap(err, "timestamp parsing failed for created_before parameter")
1✔
1568
                } else {
1✔
1569
                        query.CreatedBefore = &createdBeforeTime
×
1570
                }
×
1571
        }
1572

1573
        createdAfter := vals.Get("created_after")
8✔
1574
        if createdAfter != "" {
8✔
1575
                if createdAfterTime, err := parseEpochToTimestamp(createdAfter); err != nil {
×
1576
                        return query, errors.Wrap(err, "timestamp parsing failed created_after parameter")
×
1577
                } else {
×
1578
                        query.CreatedAfter = &createdAfterTime
×
1579
                }
×
1580
        }
1581

1582
        switch strings.ToLower(vals.Get("sort")) {
8✔
1583
        case model.SortDirectionAscending:
1✔
1584
                query.Sort = model.SortDirectionAscending
1✔
1585
        case "", model.SortDirectionDescending:
7✔
1586
                query.Sort = model.SortDirectionDescending
7✔
1587
        default:
×
1588
                return query, ErrInvalidSortDirection
×
1589
        }
1590

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

1606
        }
1607

1608
        dType := vals.Get("type")
8✔
1609
        if dType == "" {
16✔
1610
                return query, nil
8✔
1611
        }
8✔
1612
        deploymentType := model.DeploymentType(dType)
×
1613
        if deploymentType == model.DeploymentTypeSoftware ||
×
1614
                deploymentType == model.DeploymentTypeConfiguration {
×
1615
                query.Type = deploymentType
×
1616
        } else {
×
1617
                return query, errors.Errorf("unknown deployment type %s", dType)
×
1618
        }
×
1619

1620
        return query, nil
×
1621
}
1622

1623
func parseEpochToTimestamp(epoch string) (time.Time, error) {
1✔
1624
        if epochInt64, err := strconv.ParseInt(epoch, 10, 64); err != nil {
2✔
1625
                return time.Time{}, errors.Errorf("invalid timestamp: " + epoch)
1✔
1626
        } else {
1✔
1627
                return time.Unix(epochInt64, 0).UTC(), nil
×
1628
        }
×
1629
}
1630

1631
func (d *DeploymentsApiHandlers) LookupDeployment(w rest.ResponseWriter, r *rest.Request) {
9✔
1632
        ctx := r.Context()
9✔
1633
        l := requestlog.GetRequestLogger(r)
9✔
1634
        q := r.URL.Query()
9✔
1635
        defer func() {
18✔
1636
                if search := q.Get("search"); search != "" {
9✔
1637
                        q.Set("search", Redacted)
×
1638
                        r.URL.RawQuery = q.Encode()
×
1639
                }
×
1640
        }()
1641

1642
        query, err := ParseLookupQuery(q)
9✔
1643
        if err != nil {
10✔
1644
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1645
                return
1✔
1646
        }
1✔
1647

1648
        page, perPage, err := rest_utils.ParsePagination(r)
8✔
1649
        if err != nil {
9✔
1650
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1651
                return
1✔
1652
        }
1✔
1653
        query.Skip = int((page - 1) * perPage)
7✔
1654
        query.Limit = int(perPage + 1)
7✔
1655

7✔
1656
        deps, totalCount, err := d.app.LookupDeployment(ctx, query)
7✔
1657
        if err != nil {
8✔
1658
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
1✔
1659
                return
1✔
1660
        }
1✔
1661
        w.Header().Add(hdrTotalCount, strconv.FormatInt(totalCount, 10))
6✔
1662

6✔
1663
        len := len(deps)
6✔
1664
        hasNext := false
6✔
1665
        if uint64(len) > perPage {
6✔
1666
                hasNext = true
×
1667
                len = int(perPage)
×
1668
        }
×
1669

1670
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
6✔
1671
        for _, l := range links {
13✔
1672
                w.Header().Add("Link", l)
7✔
1673
        }
7✔
1674

1675
        d.view.RenderSuccessGet(w, deps[:len])
6✔
1676
}
1677

1678
func (d *DeploymentsApiHandlers) PutDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1679
        ctx := r.Context()
1✔
1680
        l := requestlog.GetRequestLogger(r)
1✔
1681

1✔
1682
        did := r.PathParam("id")
1✔
1683

1✔
1684
        idata := identity.FromContext(ctx)
1✔
1685
        if idata == nil {
1✔
1686
                d.view.RenderError(w, r, ErrMissingIdentity, http.StatusBadRequest, l)
×
1687
                return
×
1688
        }
×
1689

1690
        // reuse DeploymentLog, device and deployment IDs are ignored when
1691
        // (un-)marshaling DeploymentLog to/from JSON
1692
        var log model.DeploymentLog
1✔
1693

1✔
1694
        err := r.DecodeJsonPayload(&log)
1✔
1695
        if err != nil {
1✔
1696
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
×
1697
                return
×
1698
        }
×
1699

1700
        if err := d.app.SaveDeviceDeploymentLog(ctx, idata.Subject,
1✔
1701
                did, log.Messages); err != nil {
1✔
1702

×
1703
                if err == app.ErrModelDeploymentNotFound {
×
1704
                        d.view.RenderError(w, r, err, http.StatusNotFound, l)
×
1705
                } else {
×
1706
                        d.view.RenderInternalError(w, r, err, l)
×
1707
                }
×
1708
                return
×
1709
        }
1710

1711
        d.view.RenderEmptySuccessResponse(w)
1✔
1712
}
1713

1714
func (d *DeploymentsApiHandlers) GetDeploymentLogForDevice(w rest.ResponseWriter, r *rest.Request) {
1✔
1715
        ctx := r.Context()
1✔
1716
        l := requestlog.GetRequestLogger(r)
1✔
1717

1✔
1718
        did := r.PathParam("id")
1✔
1719
        devid := r.PathParam("devid")
1✔
1720

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

1✔
1723
        if err != nil {
1✔
1724
                d.view.RenderInternalError(w, r, err, l)
×
1725
                return
×
1726
        }
×
1727

1728
        if depl == nil {
1✔
1729
                d.view.RenderErrorNotFound(w, r, l)
×
1730
                return
×
1731
        }
×
1732

1733
        d.view.RenderDeploymentLog(w, *depl)
1✔
1734
}
1735

1736
func (d *DeploymentsApiHandlers) AbortDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
3✔
1737
        ctx := r.Context()
3✔
1738
        l := requestlog.GetRequestLogger(r)
3✔
1739

3✔
1740
        id := r.PathParam("id")
3✔
1741
        err := d.app.AbortDeviceDeployments(ctx, id)
3✔
1742

3✔
1743
        switch err {
3✔
1744
        case nil, app.ErrStorageNotFound:
2✔
1745
                d.view.RenderEmptySuccessResponse(w)
2✔
1746
        default:
1✔
1747
                d.view.RenderInternalError(w, r, err, l)
1✔
1748
        }
1749
}
1750

1751
func (d *DeploymentsApiHandlers) DeleteDeviceDeploymentsHistory(w rest.ResponseWriter,
1752
        r *rest.Request) {
3✔
1753
        ctx := r.Context()
3✔
1754
        l := requestlog.GetRequestLogger(r)
3✔
1755

3✔
1756
        id := r.PathParam("id")
3✔
1757
        err := d.app.DeleteDeviceDeploymentsHistory(ctx, id)
3✔
1758

3✔
1759
        switch err {
3✔
1760
        case nil, app.ErrStorageNotFound:
2✔
1761
                d.view.RenderEmptySuccessResponse(w)
2✔
1762
        default:
1✔
1763
                d.view.RenderInternalError(w, r, err, l)
1✔
1764
        }
1765
}
1766

1767
func (d *DeploymentsApiHandlers) ListDeviceDeployments(w rest.ResponseWriter, r *rest.Request) {
9✔
1768
        ctx := r.Context()
9✔
1769
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1770
}
9✔
1771

1772
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsInternal(w rest.ResponseWriter,
1773
        r *rest.Request) {
9✔
1774
        ctx := r.Context()
9✔
1775
        tenantID := r.PathParam("tenant")
9✔
1776
        if tenantID != "" {
18✔
1777
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1778
                        Tenant:   tenantID,
9✔
1779
                        IsDevice: true,
9✔
1780
                })
9✔
1781
        }
9✔
1782
        d.listDeviceDeployments(ctx, w, r, true)
9✔
1783
}
1784

1785
func (d *DeploymentsApiHandlers) ListDeviceDeploymentsByIDsInternal(w rest.ResponseWriter,
1786
        r *rest.Request) {
9✔
1787
        ctx := r.Context()
9✔
1788
        tenantID := r.PathParam("tenant")
9✔
1789
        if tenantID != "" {
18✔
1790
                ctx = identity.WithContext(r.Context(), &identity.Identity{
9✔
1791
                        Tenant:   tenantID,
9✔
1792
                        IsDevice: true,
9✔
1793
                })
9✔
1794
        }
9✔
1795
        d.listDeviceDeployments(ctx, w, r, false)
9✔
1796
}
1797

1798
func (d *DeploymentsApiHandlers) listDeviceDeployments(ctx context.Context,
1799
        w rest.ResponseWriter, r *rest.Request, byDeviceID bool) {
27✔
1800
        l := requestlog.GetRequestLogger(r)
27✔
1801

27✔
1802
        did := ""
27✔
1803
        var IDs []string
27✔
1804
        if byDeviceID {
45✔
1805
                did = r.PathParam("id")
18✔
1806
        } else {
27✔
1807
                values := r.URL.Query()
9✔
1808
                if values.Has("id") && len(values["id"]) > 0 {
17✔
1809
                        IDs = values["id"]
8✔
1810
                } else {
9✔
1811
                        d.view.RenderError(w, r, ErrEmptyID, http.StatusBadRequest, l)
1✔
1812
                        return
1✔
1813
                }
1✔
1814
        }
1815

1816
        page, perPage, err := rest_utils.ParsePagination(r)
26✔
1817
        if err == nil && perPage > MaximumPerPageListDeviceDeployments {
29✔
1818
                err = errors.New(rest_utils.MsgQueryParmLimit(ParamPerPage))
3✔
1819
        }
3✔
1820
        if err != nil {
32✔
1821
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
6✔
1822
                return
6✔
1823
        }
6✔
1824

1825
        lq := store.ListQueryDeviceDeployments{
20✔
1826
                Skip:     int((page - 1) * perPage),
20✔
1827
                Limit:    int(perPage),
20✔
1828
                DeviceID: did,
20✔
1829
                IDs:      IDs,
20✔
1830
        }
20✔
1831
        if status := r.URL.Query().Get("status"); status != "" {
26✔
1832
                lq.Status = &status
6✔
1833
        }
6✔
1834
        if err = lq.Validate(); err != nil {
23✔
1835
                d.view.RenderError(w, r, err, http.StatusBadRequest, l)
3✔
1836
                return
3✔
1837
        }
3✔
1838

1839
        deps, totalCount, err := d.app.GetDeviceDeploymentListForDevice(ctx, lq)
17✔
1840
        if err != nil {
20✔
1841
                d.view.RenderInternalError(w, r, err, l)
3✔
1842
                return
3✔
1843
        }
3✔
1844
        w.Header().Add(hdrTotalCount, strconv.FormatInt(int64(totalCount), 10))
14✔
1845

14✔
1846
        hasNext := totalCount > lq.Skip+len(deps)
14✔
1847
        links := rest_utils.MakePageLinkHdrs(r, page, perPage, hasNext)
14✔
1848
        for _, l := range links {
28✔
1849
                w.Header().Add("Link", l)
14✔
1850
        }
14✔
1851

1852
        d.view.RenderSuccessGet(w, deps)
14✔
1853
}
1854

1855
func (d *DeploymentsApiHandlers) AbortDeviceDeploymentsInternal(w rest.ResponseWriter,
1856
        r *rest.Request) {
×
1857
        ctx := r.Context()
×
1858
        tenantID := r.PathParam("tenantID")
×
1859
        if tenantID != "" {
×
1860
                ctx = identity.WithContext(r.Context(), &identity.Identity{
×
1861
                        Tenant:   tenantID,
×
1862
                        IsDevice: true,
×
1863
                })
×
1864
        }
×
1865

1866
        l := requestlog.GetRequestLogger(r)
×
1867

×
1868
        id := r.PathParam("id")
×
1869

×
1870
        // Decommission deployments for devices and update deployment stats
×
1871
        err := d.app.DecommissionDevice(ctx, id)
×
1872

×
1873
        switch err {
×
1874
        case nil, app.ErrStorageNotFound:
×
1875
                d.view.RenderEmptySuccessResponse(w)
×
1876
        default:
×
1877
                d.view.RenderInternalError(w, r, err, l)
×
1878

1879
        }
1880
}
1881

1882
// tenants
1883

1884
func (d *DeploymentsApiHandlers) ProvisionTenantsHandler(w rest.ResponseWriter, r *rest.Request) {
1✔
1885
        ctx := r.Context()
1✔
1886
        l := requestlog.GetRequestLogger(r)
1✔
1887

1✔
1888
        defer r.Body.Close()
1✔
1889

1✔
1890
        tenant, err := model.ParseNewTenantReq(r.Body)
1✔
1891
        if err != nil {
2✔
1892
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
1✔
1893
                return
1✔
1894
        }
1✔
1895

1896
        err = d.app.ProvisionTenant(ctx, tenant.TenantId)
1✔
1897
        if err != nil {
1✔
1898
                rest_utils.RestErrWithLogInternal(w, r, l, err)
×
1899
                return
×
1900
        }
×
1901

1902
        w.WriteHeader(http.StatusCreated)
1✔
1903
}
1904

1905
func (d *DeploymentsApiHandlers) DeploymentsPerTenantHandler(
1906
        w rest.ResponseWriter,
1907
        r *rest.Request,
1908
) {
6✔
1909
        tenantID := r.PathParam("tenant")
6✔
1910
        if tenantID == "" {
7✔
1911
                l := requestlog.GetRequestLogger(r)
1✔
1912
                rest_utils.RestErrWithLog(w, r, l, errors.New("missing tenant ID"), http.StatusBadRequest)
1✔
1913
                return
1✔
1914
        }
1✔
1915

1916
        r.Request = r.WithContext(identity.WithContext(
5✔
1917
                r.Context(),
5✔
1918
                &identity.Identity{Tenant: tenantID},
5✔
1919
        ))
5✔
1920
        d.LookupDeployment(w, r)
5✔
1921
}
1922

1923
func (d *DeploymentsApiHandlers) GetTenantStorageSettingsHandler(
1924
        w rest.ResponseWriter,
1925
        r *rest.Request,
1926
) {
5✔
1927
        l := requestlog.GetRequestLogger(r)
5✔
1928

5✔
1929
        tenantID := r.PathParam("tenant")
5✔
1930

5✔
1931
        ctx := identity.WithContext(
5✔
1932
                r.Context(),
5✔
1933
                &identity.Identity{Tenant: tenantID},
5✔
1934
        )
5✔
1935

5✔
1936
        settings, err := d.app.GetStorageSettings(ctx)
5✔
1937
        if err != nil {
7✔
1938
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1939
                return
2✔
1940
        }
2✔
1941

1942
        d.view.RenderSuccessGet(w, settings)
3✔
1943
}
1944

1945
func (d *DeploymentsApiHandlers) PutTenantStorageSettingsHandler(
1946
        w rest.ResponseWriter,
1947
        r *rest.Request,
1948
) {
10✔
1949
        l := requestlog.GetRequestLogger(r)
10✔
1950

10✔
1951
        defer r.Body.Close()
10✔
1952

10✔
1953
        tenantID := r.PathParam("tenant")
10✔
1954

10✔
1955
        ctx := identity.WithContext(
10✔
1956
                r.Context(),
10✔
1957
                &identity.Identity{Tenant: tenantID},
10✔
1958
        )
10✔
1959

10✔
1960
        settings, err := model.ParseStorageSettingsRequest(r.Body)
10✔
1961
        if err != nil {
13✔
1962
                rest_utils.RestErrWithLog(w, r, l, err, http.StatusBadRequest)
3✔
1963
                return
3✔
1964
        }
3✔
1965

1966
        err = d.app.SetStorageSettings(ctx, settings)
8✔
1967
        if err != nil {
10✔
1968
                rest_utils.RestErrWithLogInternal(w, r, l, err)
2✔
1969
                return
2✔
1970
        }
2✔
1971

1972
        w.WriteHeader(http.StatusNoContent)
6✔
1973
}
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