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

mendersoftware / mender-server / 1635240308

21 Jan 2025 01:35PM UTC coverage: 76.574% (+0.002%) from 76.572%
1635240308

push

gitlab-ci

web-flow
Merge pull request #346 from alfrunes/MEN-7939

fix: Use internal URLs for storage backend when generating artifacts

4299 of 6257 branches covered (68.71%)

Branch coverage included in aggregate %.

37 of 57 new or added lines in 5 files covered. (64.91%)

5 existing lines in 2 files now uncovered.

45366 of 58602 relevant lines covered (77.41%)

20.12 hits per line

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

11.0
/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

54
func NewEmpty(ctx context.Context, opts ...*Options) (storage.ObjectStorage, error) {
2✔
55
        opt := NewOptions(opts...)
2✔
56
        objStore := &client{
2✔
57
                bufferSize:  opt.BufferSize,
2✔
58
                contentType: opt.ContentType,
2✔
59
                proxyURL:    opt.ProxyURI,
2✔
60
        }
2✔
61
        return objStore, nil
2✔
62
}
2✔
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
        public bool,
330
) (*model.Link, error) {
×
331
        var permissions sas.BlobPermissions
×
332
        switch method {
×
333
        case http.MethodGet:
×
334
                permissions = sas.BlobPermissions{Read: true}
×
335
        case http.MethodDelete:
×
336
                permissions = sas.BlobPermissions{Delete: true}
×
337
        case http.MethodPut:
×
338
                permissions = sas.BlobPermissions{Create: true, Write: true}
×
339
        default:
×
340
                return nil, fmt.Errorf("invalid HTTP method %q", method)
×
341
        }
342
        now := time.Now().UTC()
×
343
        exp := now.Add(expire)
×
344
        // HACK: We cannot use BlockBlobClient.GetSASToken because the API does
×
345
        // not expose the required parameters.
×
346
        urlParts, _ := blob.ParseURL(blobURL)
×
347
        sk, proxyURL, err := c.signParamsFromContext(ctx)
×
348
        if err != nil {
×
349
                return nil, fmt.Errorf("failed to retrieve credentials: %w", err)
×
350
        }
×
351
        var contentDisposition string
×
352
        if filename != "" {
×
353
                contentDisposition = fmt.Sprintf(
×
354
                        `attachment; filename="%s"`, filename,
×
355
                )
×
356
        }
×
357

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

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

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

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

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

485
        return link, nil
×
486
}
487

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