Skip to content

feat(azblob): add azure blob storage provider - #257

Open
Jonas-Beck wants to merge 10 commits into
falcondev-oss:devfrom
VELUX:feature/azblob
Open

Jonas-Beck wants to merge 10 commits into
falcondev-oss:devfrom
VELUX:feature/azblob

Conversation

@Jonas-Beck

@Jonas-Beck Jonas-Beck commented Aug 4, 2026

Copy link
Copy Markdown

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/identity package DefaultAzureCredentials

Part 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.azblob values.

This means that for anyone using workload identity to authenticate, annotations should be added to the ServiceAccount using serviceAccount.annotations and labels on the pods should added using podLabels

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>
Comment thread lib/schemas.ts
@Jonas-Beck
Jonas-Beck marked this pull request as draft August 4, 2026 07:01
Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
@Jonas-Beck
Jonas-Beck marked this pull request as ready for review August 4, 2026 10:23
@DrJume DrJume linked an issue Aug 4, 2026 that may be closed by this pull request
@DrJume
DrJume requested a review from LouisHaftmann August 4, 2026 17:38

@LouisHaftmann LouisHaftmann left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. With a connection string, createDownloadUrl throws Server failed to authenticate the request when ENABLE_DIRECT_DOWNLOADS is on.
  2. The download URL always uses https://{account}.blob.core.windows.net, even when the server is configured with a different endpoint.
  3. No test calls createDownloadUrl for azblob, so CI passes without covering 1 and 2.
  4. deleteFolder('abc') also deletes abc123/....

Details are in the inline comments.

Comment thread lib/storage.ts
const startsOn = new Date()
const expiresOn = new Date(expiresAt)

const delegationKey = await this.client.getUserDelegationKey(startsOn, expiresOn)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/storage.ts
this.account,
)

return `https://${this.account}.blob.core.windows.net/${this.container}/${this.blobKey(objectName)}?${sasParams.toString()}`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/storage.ts Outdated
async deleteFolder(folderName: string): Promise<StorageDeletion> {
const deleted = { objects: 0, bytes: 0 }
const blobs = this.containerClient.listBlobsFlat({
prefix: this.blobKey(folderName),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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') // false

S3 and GCS use ${keyPrefix}/${folderName}/. countFilesInFolder and getFolderSize have the same issue.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/storage.ts Outdated
for await (const blob of blobs) {
deleted.objects++
deleted.bytes += blob.properties.contentLength ?? 0
await this.containerClient.deleteBlob(blob.name)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/schemas.ts
},
{
'STORAGE_DRIVER': type.unit('azblob'),
'STORAGE_AZBLOB_ACCOUNT': 'string',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/schemas.ts
value: {{ .container | quote }}
{{- end }}
{{- if .connectionString }}
- name: STORAGE_AZBLOB_CONNECTION_STRING

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/storage.ts Outdated
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit: Readable.from(response.readableStreamBody) would get rid of the cast.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed the cast and use Readable.from now instead.

Signed-off-by: Jonas Beck <dev@jonasbeck.dk>
@Jonas-Beck

Copy link
Copy Markdown
Author

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Azure Blob Storage driver

2 participants