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

mendersoftware / deployments / 913012443

pending completion
913012443

Pull #869

gitlab-ci

alfrunes
ci: Bump golangci-lint to v1.15.3 (go1.20)

Signed-off-by: Alf-Rune Siqveland <alf.rune@northern.tech>
Pull Request #869: feat: New configuration parameter storage.proxy_uri

179 of 253 new or added lines in 6 files covered. (70.75%)

2 existing lines in 2 files now uncovered.

7373 of 9353 relevant lines covered (78.83%)

33.73 hits per line

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

66.75
/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
        proxyURL      *url.URL
50
        bufferSize    int64
51
}
52

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

473
        return &model.Link{
2✔
474
                Uri:    uri,
2✔
475
                Expire: exp,
2✔
476
                Method: http.MethodDelete,
2✔
477
        }, nil
2✔
478
}
479

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