• 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

8.74
/backend/services/deployments/storage/azblob/azblob.go
1
// Copyright 2023 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 azblob
16

17
import (
18
        "context"
19
        "crypto/tls"
20
        "fmt"
21
        "io"
22
        "net/http"
23
        "net/url"
24
        "time"
25

26
        "github.com/mendersoftware/mender-server/services/deployments/model"
27
        "github.com/mendersoftware/mender-server/services/deployments/storage"
28
        "github.com/mendersoftware/mender-server/services/deployments/utils"
29

30
        "github.com/Azure/azure-sdk-for-go/sdk/azcore"
31
        "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
32
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
33
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
34
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
35
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
36
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
37
        "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
38
)
39

40
const (
41
        headerBlobType = "x-ms-blob-type"
42

43
        blobTypeBlock = "BlockBlob"
44
)
45

46
type client struct {
47
        DefaultClient *container.Client
48
        credentials   *azblob.SharedKeyCredential
49
        contentType   *string
50
        proxyURL      *url.URL
51
        bufferSize    int64
52
}
53

UNCOV
54
func NewEmpty(ctx context.Context, opts ...*Options) (storage.ObjectStorage, error) {
×
UNCOV
55
        opt := NewOptions(opts...)
×
UNCOV
56
        objStore := &client{
×
UNCOV
57
                bufferSize:  opt.BufferSize,
×
UNCOV
58
                contentType: opt.ContentType,
×
UNCOV
59
                proxyURL:    opt.ProxyURI,
×
UNCOV
60
        }
×
UNCOV
61
        return objStore, nil
×
UNCOV
62
}
×
63

64
func New(ctx context.Context, bucket string, opts ...*Options) (storage.ObjectStorage, error) {
×
65
        var (
×
66
                err    error
×
67
                cc     *container.Client
×
68
                azCred *azblob.SharedKeyCredential
×
69
        )
×
70
        opt := NewOptions(opts...)
×
71
        objectStorage, err := NewEmpty(ctx, opt)
×
72
        if err != nil {
×
73
                return nil, err
×
74
        }
×
75
        clientOptions := &container.ClientOptions{
×
76
                ClientOptions: azcore.ClientOptions{
×
77
                        Transport: &http.Client{
×
78
                                Transport: &http.Transport{
×
79
                                        TLSClientConfig: &tls.Config{
×
80
                                                RootCAs: storage.GetRootCAs(),
×
81
                                        },
×
82
                                },
×
83
                        },
×
84
                },
×
85
        }
×
86
        if opt.ConnectionString != nil {
×
87
                cc, err = container.NewClientFromConnectionString(
×
88
                        *opt.ConnectionString, bucket, clientOptions,
×
89
                )
×
90
                if err == nil {
×
91
                        azCred, err = keyFromConnString(*opt.ConnectionString)
×
92
                }
×
93
        } else if sk := opt.SharedKey; sk != nil {
×
94
                var containerURL string
×
95
                containerURL, azCred, err = sk.azParams(bucket)
×
96
                if err == nil {
×
97
                        cc, err = container.NewClientWithSharedKeyCredential(
×
98
                                containerURL,
×
99
                                azCred,
×
100
                                clientOptions,
×
101
                        )
×
102
                }
×
103
        }
104
        if err != nil {
×
105
                return nil, err
×
106
        }
×
107
        objectStorage.(*client).DefaultClient = cc
×
108
        objectStorage.(*client).credentials = azCred
×
109
        if err := objectStorage.HealthCheck(ctx); err != nil {
×
110
                return nil, err
×
111
        }
×
112
        return objectStorage, nil
×
113
}
114

115
func (c *client) clientFromContext(
116
        ctx context.Context,
117
) (client *container.Client, err error) {
1✔
118
        client = c.DefaultClient
1✔
119
        if settings, _ := storage.SettingsFromContext(ctx); settings != nil {
2✔
120
                if err = settings.Validate(); err != nil {
2✔
121
                        return nil, err
1✔
122
                } else if settings.ConnectionString != nil {
1✔
123
                        client, err = container.NewClientFromConnectionString(
×
124
                                *settings.ConnectionString,
×
125
                                settings.Bucket,
×
126
                                &container.ClientOptions{},
×
127
                        )
×
128
                } else {
×
129
                        var (
×
130
                                containerURL string
×
131
                                azCreds      *azblob.SharedKeyCredential
×
132
                        )
×
133
                        creds := SharedKeyCredentials{
×
134
                                AccountName: settings.Key,
×
135
                                AccountKey:  settings.Secret,
×
136
                        }
×
137
                        if settings.Uri != "" {
×
138
                                creds.URI = &settings.Uri
×
139
                        }
×
140

141
                        containerURL, azCreds, err = creds.azParams(settings.Bucket)
×
142
                        if err == nil {
×
143
                                client, err = container.NewClientWithSharedKeyCredential(
×
144
                                        containerURL,
×
145
                                        azCreds,
×
146
                                        &container.ClientOptions{},
×
147
                                )
×
148
                        }
×
149
                }
150
        }
151
        if client == nil {
1✔
152
                return nil, ErrEmptyClient
×
153
        }
×
154
        return client, err
1✔
155
}
156

157
func (c *client) HealthCheck(ctx context.Context) error {
×
158
        azClient, err := c.clientFromContext(ctx)
×
159
        if err != nil {
×
160
                if err == ErrEmptyClient {
×
161
                        return nil
×
162
                }
×
163
                return OpError{
×
164
                        Op:     OpHealthCheck,
×
165
                        Reason: err,
×
166
                }
×
167
        }
168
        _, err = azClient.GetProperties(ctx, &container.GetPropertiesOptions{})
×
169
        if err != nil {
×
170
                return OpError{
×
171
                        Op:     OpHealthCheck,
×
172
                        Reason: err,
×
173
                }
×
174
        }
×
175
        return nil
×
176
}
177

178
type objectReader struct {
179
        io.ReadCloser
180
        length int64
181
}
182

183
func (r objectReader) Length() int64 {
×
184
        return r.length
×
185
}
×
186

187
func (c *client) GetObject(
188
        ctx context.Context,
189
        objectPath string,
190
) (io.ReadCloser, error) {
1✔
191
        azClient, err := c.clientFromContext(ctx)
1✔
192
        if err != nil {
2✔
193
                return nil, OpError{
1✔
194
                        Op:     OpGetObject,
1✔
195
                        Reason: err,
1✔
196
                }
1✔
197
        }
1✔
198
        bc := azClient.NewBlockBlobClient(objectPath)
1✔
199
        out, err := bc.DownloadStream(ctx, &blob.DownloadStreamOptions{})
1✔
200
        if bloberror.HasCode(err,
1✔
201
                bloberror.BlobNotFound,
1✔
202
                bloberror.ContainerNotFound,
1✔
203
                bloberror.ResourceNotFound) {
1✔
204
                err = storage.ErrObjectNotFound
×
205
        }
×
206
        if err != nil {
2✔
207
                return nil, OpError{
1✔
208
                        Op:     OpGetObject,
1✔
209
                        Reason: err,
1✔
210
                }
1✔
211
        }
1✔
212
        if out.ContentLength != nil {
2✔
213
                return objectReader{
1✔
214
                        ReadCloser: out.Body,
1✔
215
                        length:     *out.ContentLength,
1✔
216
                }, nil
1✔
217
        }
1✔
218
        return out.Body, nil
×
219
}
220

221
func (c *client) PutObject(
222
        ctx context.Context,
223
        objectPath string,
224
        src io.Reader,
225
) error {
×
226
        azClient, err := c.clientFromContext(ctx)
×
227
        if err != nil {
×
228
                return OpError{
×
229
                        Op:     OpPutObject,
×
230
                        Reason: err,
×
231
                }
×
232
        }
×
233
        bc := azClient.NewBlockBlobClient(objectPath)
×
234
        var blobOpts = &blockblob.UploadStreamOptions{
×
235
                HTTPHeaders: &blob.HTTPHeaders{
×
236
                        BlobContentType: c.contentType,
×
237
                },
×
238
        }
×
239
        blobOpts.BlockSize = c.bufferSize
×
240
        _, err = bc.UploadStream(ctx, src, blobOpts)
×
241
        if err != nil {
×
242
                return OpError{
×
243
                        Op:      OpPutObject,
×
244
                        Message: "failed to upload object to blob",
×
245
                        Reason:  err,
×
246
                }
×
247
        }
×
248
        return err
×
249
}
250

251
func (c *client) DeleteObject(
252
        ctx context.Context,
253
        path string,
254
) error {
×
255
        azClient, err := c.clientFromContext(ctx)
×
256
        if err != nil {
×
257
                return OpError{
×
258
                        Op:     OpDeleteObject,
×
259
                        Reason: err,
×
260
                }
×
261
        }
×
262
        bc := azClient.NewBlockBlobClient(path)
×
263
        _, err = bc.Delete(ctx, &blob.DeleteOptions{
×
264
                DeleteSnapshots: to.Ptr(azblob.DeleteSnapshotsOptionTypeInclude),
×
265
        })
×
266
        if bloberror.HasCode(err,
×
267
                bloberror.BlobNotFound,
×
268
                bloberror.ContainerNotFound,
×
269
                bloberror.ResourceNotFound) {
×
270
                err = storage.ErrObjectNotFound
×
271
        }
×
272
        if err != nil {
×
273
                return OpError{
×
274
                        Op:      OpDeleteObject,
×
275
                        Message: "failed to delete object",
×
276
                        Reason:  err,
×
277
                }
×
278
        }
×
279
        return nil
×
280
}
281

282
func (c *client) StatObject(
283
        ctx context.Context,
284
        path string,
285
) (*storage.ObjectInfo, error) {
×
286
        azClient, err := c.clientFromContext(ctx)
×
287
        if err != nil {
×
288
                return nil, OpError{
×
289
                        Op:     OpStatObject,
×
290
                        Reason: err,
×
291
                }
×
292
        }
×
293
        bc := azClient.NewBlockBlobClient(path)
×
294
        if err != nil {
×
295
                return nil, OpError{
×
296
                        Op:      OpStatObject,
×
297
                        Message: "failed to initialize blob client",
×
298
                        Reason:  err,
×
299
                }
×
300
        }
×
301
        rsp, err := bc.GetProperties(ctx, &blob.GetPropertiesOptions{})
×
302
        if bloberror.HasCode(err,
×
303
                bloberror.BlobNotFound,
×
304
                bloberror.ContainerNotFound,
×
305
                bloberror.ResourceNotFound,
×
306
        ) {
×
307
                err = storage.ErrObjectNotFound
×
308
        }
×
309
        if err != nil {
×
310
                return nil, OpError{
×
311
                        Op:      OpStatObject,
×
312
                        Message: "failed to retrieve object properties",
×
313
                        Reason:  err,
×
314
                }
×
315
        }
×
316
        return &storage.ObjectInfo{
×
317
                Path:         path,
×
318
                LastModified: rsp.LastModified,
×
319
                Size:         rsp.ContentLength,
×
320
        }, nil
×
321
}
322

323
func (c *client) buildSignedURL(
324
        ctx context.Context,
325
        method string,
326
        blobURL string,
327
        expire time.Duration,
328
        filename string,
329
) (*model.Link, error) {
×
330
        var permissions sas.BlobPermissions
×
331
        switch method {
×
332
        case http.MethodGet:
×
333
                permissions = sas.BlobPermissions{Read: true}
×
334
        case http.MethodDelete:
×
335
                permissions = sas.BlobPermissions{Delete: true}
×
336
        case http.MethodPut:
×
337
                permissions = sas.BlobPermissions{Create: true, Write: true}
×
338
        default:
×
339
                return nil, fmt.Errorf("invalid HTTP method %q", method)
×
340
        }
341
        now := time.Now().UTC()
×
342
        exp := now.Add(expire)
×
343
        // HACK: We cannot use BlockBlobClient.GetSASToken because the API does
×
344
        // not expose the required parameters.
×
345
        urlParts, _ := blob.ParseURL(blobURL)
×
346
        sk, proxyURL, err := c.signParamsFromContext(ctx)
×
347
        if err != nil {
×
348
                return nil, fmt.Errorf("failed to retrieve credentials: %w", err)
×
349
        }
×
350
        var contentDisposition string
×
351
        if filename != "" {
×
352
                contentDisposition = fmt.Sprintf(
×
353
                        `attachment; filename="%s"`, filename,
×
354
                )
×
355
        }
×
356

357
        qParams, err := sas.BlobSignatureValues{
×
358
                ContainerName: urlParts.ContainerName,
×
359
                BlobName:      urlParts.BlobName,
×
360

×
361
                Permissions:        permissions.String(),
×
362
                ContentDisposition: contentDisposition,
×
363

×
364
                StartTime:  now.UTC(),
×
365
                ExpiryTime: exp.UTC(),
×
366
        }.SignWithSharedKey(sk)
×
367
        if err != nil {
×
368
                return nil, err
×
369
        }
×
370
        baseURL, err := url.Parse(blobURL)
×
371
        if err != nil {
×
372
                return nil, err
×
373
        }
×
374
        qSAS, err := url.ParseQuery(qParams.Encode())
×
375
        if err != nil {
×
376
                return nil, err
×
377
        }
×
378
        q := baseURL.Query()
×
379
        for key, values := range qSAS {
×
380
                for _, value := range values {
×
381
                        q.Add(key, value)
×
382
                }
×
383
        }
384
        baseURL.RawQuery = q.Encode()
×
385
        baseURL, err = utils.RewriteProxyURL(baseURL, proxyURL)
×
386
        if err != nil {
×
387
                return nil, err
×
388
        }
×
389
        return &model.Link{
×
390
                Expire: exp,
×
391
                Method: method,
×
392
                Uri:    baseURL.String(),
×
393
        }, nil
×
394
}
395

396
func (c *client) GetRequest(
397
        ctx context.Context,
398
        objectPath string,
399
        filename string,
400
        duration time.Duration,
401
) (*model.Link, error) {
×
402
        azClient, err := c.clientFromContext(ctx)
×
403
        if err != nil {
×
404
                return nil, OpError{
×
405
                        Op:     OpGetRequest,
×
406
                        Reason: err,
×
407
                }
×
408
        }
×
409
        // Check if object exists
410
        bc := azClient.NewBlockBlobClient(objectPath)
×
411
        if err != nil {
×
412
                return nil, OpError{
×
413
                        Op:      OpGetRequest,
×
414
                        Message: "failed to initialize blob client",
×
415
                        Reason:  err,
×
416
                }
×
417
        }
×
418
        _, err = bc.GetProperties(ctx, &blob.GetPropertiesOptions{})
×
419
        if bloberror.HasCode(err,
×
420
                bloberror.BlobNotFound,
×
421
                bloberror.ContainerNotFound,
×
422
                bloberror.ResourceNotFound,
×
423
        ) {
×
424
                err = storage.ErrObjectNotFound
×
425
        }
×
426
        if err != nil {
×
427
                return nil, OpError{
×
428
                        Op:      OpGetRequest,
×
429
                        Message: "failed to check preconditions",
×
430
                        Reason:  err,
×
431
                }
×
432
        }
×
433
        link, err := c.buildSignedURL(
×
434
                ctx,
×
435
                http.MethodGet,
×
436
                bc.URL(),
×
437
                duration,
×
438
                filename,
×
439
        )
×
440
        if err != nil {
×
441
                return nil, OpError{
×
442
                        Op:      OpGetRequest,
×
443
                        Message: "failed to create pre-signed URL",
×
444
                        Reason:  err,
×
445
                }
×
446
        }
×
447
        return link, nil
×
448
}
449

450
func (c *client) DeleteRequest(
451
        ctx context.Context,
452
        path string,
453
        duration time.Duration,
454
) (*model.Link, error) {
×
455
        azClient, err := c.clientFromContext(ctx)
×
456
        if err != nil {
×
457
                return nil, OpError{
×
458
                        Op:     OpGetRequest,
×
459
                        Reason: err,
×
460
                }
×
461
        }
×
462
        bc := azClient.NewBlobClient(path)
×
463
        if err != nil {
×
464
                return nil, OpError{
×
465
                        Op:      OpDeleteRequest,
×
466
                        Message: "failed to initialize blob client",
×
467
                        Reason:  err,
×
468
                }
×
469
        }
×
470
        link, err := c.buildSignedURL(ctx, http.MethodDelete, bc.URL(), duration, "")
×
471
        if err != nil {
×
472
                return nil, OpError{
×
473
                        Op:      OpDeleteRequest,
×
474
                        Message: "failed to generate signed URL",
×
475
                        Reason:  err,
×
476
                }
×
477
        }
×
478

479
        return link, nil
×
480
}
481

482
func (c *client) PutRequest(
483
        ctx context.Context,
484
        objectPath string,
485
        duration time.Duration,
486
) (*model.Link, error) {
×
487
        azClient, err := c.clientFromContext(ctx)
×
488
        if err != nil {
×
489
                return nil, OpError{
×
490
                        Op:     OpGetRequest,
×
491
                        Reason: err,
×
492
                }
×
493
        }
×
494
        bc := azClient.NewBlobClient(objectPath)
×
495
        if err != nil {
×
496
                return nil, OpError{
×
497
                        Op:      OpPutRequest,
×
498
                        Message: "failed to initialize blob client",
×
499
                        Reason:  err,
×
500
                }
×
501
        }
×
502
        link, err := c.buildSignedURL(ctx, http.MethodPut, bc.URL(), duration, "")
×
503
        if err != nil {
×
504
                return nil, OpError{
×
505
                        Op:      OpPutRequest,
×
506
                        Message: "failed to generate signed URL",
×
507
                        Reason:  err,
×
508
                }
×
509
        }
×
510
        link.Header = map[string]string{
×
511
                headerBlobType: blobTypeBlock,
×
512
        }
×
513
        return link, nil
×
514
}
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