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

mendersoftware / deployments / 920043239

pending completion
920043239

Pull #872

gitlab-ci

alfrunes
chore: Restrict tag character set

Changelog: None
Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #872: MEN-6348: Add tags to releases

220 of 229 new or added lines in 7 files covered. (96.07%)

223 existing lines in 7 files now uncovered.

7560 of 9480 relevant lines covered (79.75%)

34.07 hits per line

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

67.59
/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/deployments/model"
27
        "github.com/mendersoftware/deployments/storage"
28

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

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

42
        blobTypeBlock = "BlockBlob"
43
)
44

45
type client struct {
46
        DefaultClient *container.Client
47
        credentials   *azblob.SharedKeyCredential
48
        contentType   *string
49
        bufferSize    int64
50
}
51

52
func NewEmpty(ctx context.Context, opts ...*Options) (storage.ObjectStorage, error) {
3✔
53
        opt := NewOptions(opts...)
3✔
54
        objStore := &client{
3✔
55
                bufferSize:  opt.BufferSize,
3✔
56
                contentType: opt.ContentType,
3✔
57
        }
3✔
58
        return objStore, nil
3✔
59
}
3✔
60

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

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

138
                        containerURL, azCreds, err = creds.azParams(settings.Bucket)
11✔
139
                        if err == nil {
22✔
140
                                client, err = container.NewClientWithSharedKeyCredential(
11✔
141
                                        containerURL,
11✔
142
                                        azCreds,
11✔
143
                                        &container.ClientOptions{},
11✔
144
                                )
11✔
145
                        }
11✔
146
                }
147
        }
148
        if client == nil {
27✔
149
                return nil, ErrEmptyClient
1✔
150
        }
1✔
151
        return client, err
25✔
152
}
153

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

175
type objectReader struct {
176
        io.ReadCloser
177
        length int64
178
}
179

UNCOV
180
func (r objectReader) Length() int64 {
×
UNCOV
181
        return r.length
×
UNCOV
182
}
×
183

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

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

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

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

320
func buildSignedURL(
321
        blobURL string,
322
        SASParams sas.QueryParameters,
323
) (string, error) {
2✔
324
        baseURL, err := url.Parse(blobURL)
2✔
325
        if err != nil {
2✔
UNCOV
326
                return "", err
×
UNCOV
327
        }
×
328
        qSAS, err := url.ParseQuery(SASParams.Encode())
2✔
329
        if err != nil {
2✔
UNCOV
330
                return "", err
×
UNCOV
331
        }
×
332
        q := baseURL.Query()
2✔
333
        for key, values := range qSAS {
16✔
334
                for _, value := range values {
28✔
335
                        q.Add(key, value)
14✔
336
                }
14✔
337
        }
338
        baseURL.RawQuery = q.Encode()
2✔
339
        return baseURL.String(), nil
2✔
340
}
341

342
func (c *client) GetRequest(
343
        ctx context.Context,
344
        objectPath string,
345
        filename string,
346
        duration time.Duration,
347
) (*model.Link, error) {
4✔
348
        azClient, err := c.clientFromContext(ctx)
4✔
349
        if err != nil {
4✔
UNCOV
350
                return nil, OpError{
×
UNCOV
351
                        Op:     OpGetRequest,
×
UNCOV
352
                        Reason: err,
×
UNCOV
353
                }
×
UNCOV
354
        }
×
355
        // Check if object exists
356
        bc := azClient.NewBlockBlobClient(objectPath)
4✔
357
        if err != nil {
4✔
UNCOV
358
                return nil, OpError{
×
UNCOV
359
                        Op:      OpGetRequest,
×
UNCOV
360
                        Message: "failed to initialize blob client",
×
UNCOV
361
                        Reason:  err,
×
UNCOV
362
                }
×
UNCOV
363
        }
×
364
        _, err = bc.GetProperties(ctx, &blob.GetPropertiesOptions{})
4✔
365
        if bloberror.HasCode(err,
4✔
366
                bloberror.BlobNotFound,
4✔
367
                bloberror.ContainerNotFound,
4✔
368
                bloberror.ResourceNotFound,
4✔
369
        ) {
6✔
370
                err = storage.ErrObjectNotFound
2✔
371
        }
2✔
372
        if err != nil {
6✔
373
                return nil, OpError{
2✔
374
                        Op:      OpGetRequest,
2✔
375
                        Message: "failed to check preconditions",
2✔
376
                        Reason:  err,
2✔
377
                }
2✔
378
        }
2✔
379
        now := time.Now().UTC()
2✔
380
        exp := now.Add(duration)
2✔
381
        // HACK: We cannot use BlockBlobClient.GetSASToken because the API does
2✔
382
        // not expose the required parameters.
2✔
383
        urlParts, _ := blob.ParseURL(bc.URL())
2✔
384
        sk, err := c.credentialsFromContext(ctx)
2✔
385
        if err != nil {
2✔
UNCOV
386
                return nil, OpError{
×
387
                        Op:      OpGetRequest,
×
388
                        Message: "failed to retrieve credentials",
×
UNCOV
389
                        Reason:  err,
×
UNCOV
390
                }
×
UNCOV
391
        }
×
392
        var contentDisposition string
2✔
393
        if filename != "" {
4✔
394
                contentDisposition = fmt.Sprintf(
2✔
395
                        `attachment; filename="%s"`, filename,
2✔
396
                )
2✔
397
        }
2✔
398
        permissions := &sas.BlobPermissions{
2✔
399
                Read: true,
2✔
400
        }
2✔
401
        qParams, err := sas.BlobSignatureValues{
2✔
402
                ContainerName: urlParts.ContainerName,
2✔
403
                BlobName:      urlParts.BlobName,
2✔
404

2✔
405
                Permissions:        permissions.String(),
2✔
406
                ContentDisposition: contentDisposition,
2✔
407

2✔
408
                StartTime:  now.UTC(),
2✔
409
                ExpiryTime: exp.UTC(),
2✔
410
        }.SignWithSharedKey(sk)
2✔
411
        if err != nil {
2✔
412
                return nil, OpError{
×
413
                        Op:      OpGetRequest,
×
414
                        Message: "failed to build signed URL",
×
415
                        Reason:  err,
×
416
                }
×
417
        }
×
418
        uri, err := buildSignedURL(bc.URL(), qParams)
2✔
419
        if err != nil {
2✔
UNCOV
420
                return nil, OpError{
×
UNCOV
421
                        Op:      OpGetRequest,
×
UNCOV
422
                        Message: "failed to create pre-signed URL",
×
UNCOV
423
                        Reason:  err,
×
UNCOV
424
                }
×
UNCOV
425
        }
×
426
        return &model.Link{
2✔
427
                Uri:    uri,
2✔
428
                Expire: exp,
2✔
429
                Method: http.MethodGet,
2✔
430
        }, nil
2✔
431
}
432

433
func (c *client) DeleteRequest(
434
        ctx context.Context,
435
        path string,
436
        duration time.Duration,
437
) (*model.Link, error) {
2✔
438
        azClient, err := c.clientFromContext(ctx)
2✔
439
        if err != nil {
2✔
UNCOV
440
                return nil, OpError{
×
441
                        Op:     OpGetRequest,
×
442
                        Reason: err,
×
443
                }
×
444
        }
×
445
        bc := azClient.NewBlobClient(path)
2✔
446
        if err != nil {
2✔
UNCOV
447
                return nil, OpError{
×
UNCOV
448
                        Op:      OpDeleteRequest,
×
UNCOV
449
                        Message: "failed to initialize blob client",
×
UNCOV
450
                        Reason:  err,
×
UNCOV
451
                }
×
UNCOV
452
        }
×
453
        now := time.Now().UTC()
2✔
454
        exp := now.Add(duration)
2✔
455
        uri, err := bc.GetSASURL(sas.BlobPermissions{Delete: true}, now, exp)
2✔
456
        if err != nil {
2✔
457
                return nil, OpError{
×
458
                        Op:      OpDeleteRequest,
×
459
                        Message: "failed to generate signed URL",
×
460
                        Reason:  err,
×
461
                }
×
UNCOV
462
        }
×
463

464
        return &model.Link{
2✔
465
                Uri:    uri,
2✔
466
                Expire: exp,
2✔
467
                Method: http.MethodDelete,
2✔
468
        }, nil
2✔
469
}
470

471
func (c *client) PutRequest(
472
        ctx context.Context,
473
        objectPath string,
474
        duration time.Duration,
475
) (*model.Link, error) {
2✔
476
        azClient, err := c.clientFromContext(ctx)
2✔
477
        if err != nil {
2✔
UNCOV
478
                return nil, OpError{
×
UNCOV
479
                        Op:     OpGetRequest,
×
UNCOV
480
                        Reason: err,
×
UNCOV
481
                }
×
UNCOV
482
        }
×
483
        bc := azClient.NewBlobClient(objectPath)
2✔
484
        if err != nil {
2✔
UNCOV
485
                return nil, OpError{
×
UNCOV
486
                        Op:      OpPutRequest,
×
UNCOV
487
                        Message: "failed to initialize blob client",
×
UNCOV
488
                        Reason:  err,
×
489
                }
×
490
        }
×
491
        now := time.Now().UTC()
2✔
492
        exp := now.Add(duration)
2✔
493
        uri, err := bc.GetSASURL(sas.BlobPermissions{
2✔
494
                Create: true,
2✔
495
                Write:  true,
2✔
496
        }, now, exp)
2✔
497
        if err != nil {
2✔
498
                return nil, OpError{
×
499
                        Op:      OpPutRequest,
×
500
                        Message: "failed to generate signed URL",
×
501
                        Reason:  err,
×
UNCOV
502
                }
×
UNCOV
503
        }
×
504
        hdrs := map[string]string{
2✔
505
                headerBlobType: blobTypeBlock,
2✔
506
        }
2✔
507
        return &model.Link{
2✔
508
                Uri:    uri,
2✔
509
                Expire: exp,
2✔
510
                Method: http.MethodPut,
2✔
511
                Header: hdrs,
2✔
512
        }, nil
2✔
513
}
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