feat(azblob): add azure blob storage provider - #257
Jonas-Beck wants to merge 10 commits into
Conversation
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
LouisHaftmann
left a comment
There was a problem hiding this comment.
Sorry for leaving this without a response for so long, I couldn't find the time to look at it properly. Thanks for your patience, and for the PR. The adapter fits well with the existing ones.
I checked out the branch and ran it against Azurite. Four things need fixing before merge. Numbers 1, 2 and 4 are bugs, and 3 is the missing test that would have caught 1 and 2. The rest are smaller.
- With a connection string,
createDownloadUrlthrowsServer failed to authenticate the requestwhenENABLE_DIRECT_DOWNLOADSis on. - The download URL always uses
https://{account}.blob.core.windows.net, even when the server is configured with a different endpoint. - No test calls
createDownloadUrlfor azblob, so CI passes without covering 1 and 2. deleteFolder('abc')also deletesabc123/....
Details are in the inline comments.
| const startsOn = new Date() | ||
| const expiresOn = new Date(expiresAt) | ||
|
|
||
| const delegationKey = await this.client.getUserDelegationKey(startsOn, expiresOn) |
There was a problem hiding this comment.
It looks like getUserDelegationKey only works with Entra ID credentials, so with a connection string it fails:
Server failed to authenticate the request. Make sure the value of the Authorization header is formed correctly including the signature.
I tried blobClient.generateSasUrl() for the connection string case, and the URL it generated worked. Maybe use that there and keep the delegation key for DefaultAzureCredential?
Also, setting startsOn to exactly now can cause 403s if the clocks are slightly off. It's probably safer to leave it out or set it a few minutes back.
| this.account, | ||
| ) | ||
|
|
||
| return `https://${this.account}.blob.core.windows.net/${this.container}/${this.blobKey(objectName)}?${sasParams.toString()}` |
There was a problem hiding this comment.
This one ignores STORAGE_AZBLOB_ENDPOINT and the endpoint from the connection string. On Azurite I got https://devstoreaccount1.blob.core.windows.net/... back, but the blob actually lives at http://localhost:10000/devstoreaccount1/.... Building the URL from getBlobClient(key).url should fix it.
| async deleteFolder(folderName: string): Promise<StorageDeletion> { | ||
| const deleted = { objects: 0, bytes: 0 } | ||
| const blobs = this.containerClient.listBlobsFlat({ | ||
| prefix: this.blobKey(folderName), |
There was a problem hiding this comment.
I think the prefix needs a trailing slash here. Without it, other folders that start with the same name match too:
await adapter.uploadStream('abc/merged', ...)
await adapter.uploadStream('abc123/merged', ...)
await adapter.deleteFolder('abc') // { objects: 2, bytes: 2 }
await adapter.objectExists('abc123/merged') // falseS3 and GCS use ${keyPrefix}/${folderName}/. countFilesInFolder and getFolderSize have the same issue.
There was a problem hiding this comment.
The prefix has been updated to include trailing slash for deleteFolder / countFilesInFolder / getFolderSize. I have also added test case for this, that is run on all storage providers.
| for await (const blob of blobs) { | ||
| deleted.objects++ | ||
| deleted.bytes += blob.properties.contentLength ?? 0 | ||
| await this.containerClient.deleteBlob(blob.name) |
There was a problem hiding this comment.
This deletes blobs one request at a time, which gets slow for entries with lots of parts. BlobBatchClient.deleteBlobs can do up to 256 per request, and the same would help in clear().
There was a problem hiding this comment.
I have updated it to make use of BlobBatchClient for bulk deletion of blobs.
It will use the max request limit(256) as the page size when listing blobs and then perform batch deletion for each page.
| }, | ||
| { | ||
| 'STORAGE_DRIVER': type.unit('azblob'), | ||
| 'STORAGE_AZBLOB_ACCOUNT': 'string', |
There was a problem hiding this comment.
Since the connection string already includes the account name, it'd be nice not to require STORAGE_AZBLOB_ACCOUNT as well. Maybe split this into a connection string branch and an account + DefaultAzureCredential branch? The adapter can get the account name from client.accountName.
| value: {{ .container | quote }} | ||
| {{- end }} | ||
| {{- if .connectionString }} | ||
| - name: STORAGE_AZBLOB_CONNECTION_STRING |
There was a problem hiding this comment.
This ends up putting the account key into the Deployment as plain text. What do you think about leaving connectionString out of values.yaml and pointing people to existingSecret instead?
There was a problem hiding this comment.
Makes sense to me, should never really be configured directly in values. But from what I can see this is the pattern used by other secrets as well e.g db.postgres.password
For now I have removed connectionString from both values and helpers. Also mentioned the connection string as example for the existingSecret.
| const response = await blockBlobClient.download() | ||
| if (!response.readableStreamBody) throw new Error(`No stream for blob "${objectName}"`) | ||
| // Casting from NodeJS.ReadableStream to Readable | ||
| return response.readableStreamBody as Readable |
There was a problem hiding this comment.
Just a nit: Readable.from(response.readableStreamBody) would get rid of the cast.
There was a problem hiding this comment.
I have removed the cast and use Readable.from now instead.
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
|
Thanks for the review, I will get the PR updated |
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
3264e68 to
b64aa95
Compare
This PR adds support for using azure blob storage as a storage provider
This has support for authentication using connection string and also authentication using the
@azure/identitypackage DefaultAzureCredentialsPart of #256
helmchart has also been updated to support configuring the new env variables for azblob storage driver, currently any authentication related configuration when using DefaultAzureCredentials are not exposed in the
storage.azblobvalues.This means that for anyone using workload identity to authenticate, annotations should be added to the ServiceAccount using
serviceAccount.annotationsand labels on the pods should added usingpodLabels