Skip to content

Adding the SDK Binding Support for Storage Blob #341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c3f43cc
Adding SDK Binding for BlobTrigger
Apr 24, 2025
d5ed6aa
Adding Supported Binding Types Enum
Apr 24, 2025
0541ff5
Adding MI support
Apr 27, 2025
4e6848d
Renaming and Creating a Factory for StorageBlobClient
Apr 28, 2025
4b154d0
Adding support for input binding
Apr 28, 2025
3e98a8b
Adding unit tests
Apr 28, 2025
bde73cd
Removing Log Statements
Apr 28, 2025
80b8618
Updating the package.json for sinon testing dependencies
Apr 28, 2025
0e7efad
Original Without Singleton
May 5, 2025
cd584ba
Moving the StorageClientFactory to extensions base
May 9, 2025
a50c485
Updating package.json
May 9, 2025
81f6c60
Removing extra log
May 9, 2025
caa39e1
Adding generic call to support deferred binding
May 12, 2025
d1332ec
Code Review Comments
May 12, 2025
4dca789
Adding thje logs for Dashboard at function start
May 13, 2025
057e790
Fixing Package-loc.json
May 13, 2025
a5cc33b
Fixing Linting issue and tests
May 13, 2025
2c81f81
Ensure the release version is in the constant file
May 13, 2025
ebbc2ea
Adding SDK Binding for BlobTrigger
Apr 24, 2025
580212a
Adding Supported Binding Types Enum
Apr 24, 2025
380c3dd
Adding MI support
Apr 27, 2025
cf1be60
Renaming and Creating a Factory for StorageBlobClient
Apr 28, 2025
dfdafd9
Package.json conflict resolution
May 14, 2025
e2d7acb
Merge branch 'swapnil/SdkBindingBlob' of https://github.com/Azure/azu…
May 14, 2025
1d8c6f5
Removing the unecessory log
May 15, 2025
8c85817
Removing unecessory changes
May 15, 2025
326454d
Removing AzuriteConfig
May 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
},
{
"name": "Current TS Tests File",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"args": ["-r", "ts-node/register", "${relativeFile}"],
"cwd": "${workspaceRoot}",
"protocol": "inspector"
},
{
"name": "mocha tests",
"type": "node",
"protocol": "inspector",
"request": "launch",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"args": [
"--require",
"ts-node/register",
"${workspaceRoot}/test/**/toCoreFunctionMetadata.test.ts",
"--no-timeouts"
],
"cwd": "${workspaceRoot}"
}
]
}
2,338 changes: 1,986 additions & 352 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@
"watch": "webpack --watch --mode development"
},
"dependencies": {
"@azure/identity": "^4.9.1",
"@azure/storage-blob": "^12.27.0",
"cookie": "^0.7.0",
"long": "^4.0.0",
"sinon": "^20.0.0",
"undici": "^5.13.0"
},
"devDependencies": {
Expand All @@ -55,6 +58,7 @@
"@types/mocha": "^9.1.1",
"@types/node": "^18.0.0",
"@types/semver": "^7.3.9",
"@types/sinon": "^17.0.4",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"chai": "^4.2.0",
Expand All @@ -65,20 +69,20 @@
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-webpack-plugin": "^3.2.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-webpack-plugin": "^3.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.13",
"fs-extra": "^10.0.1",
"globby": "^11.0.0",
"minimist": "^1.2.6",
"mocha": "^9.1.1",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.0.2",
"mocha-multi-reporters": "^1.5.1",
"prettier": "^2.4.1",
"semver": "^7.3.5",
"ts-loader": "^9.3.1",
"ts-node": "^3.3.0",
"typescript": "^4.5.5",
"typescript": "^4.9.5",
"typescript4": "npm:typescript@~4.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
Expand Down
2 changes: 0 additions & 2 deletions src/InvocationModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,9 @@ export class InvocationModel implements coreTypes.InvocationModel {
} else {
input = fromRpcTypedData(binding.data);
}

if (isTimerTrigger(bindingType)) {
input = toCamelCaseValue(input);
}

if (isTrigger(bindingType)) {
inputs.push(input);
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/converters/fromRpcTypedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import { RpcTypedData } from '@azure/functions-core';
import { AzureStorageBlobClientFactory } from '../deferred-binding/storage-blob/azureStorageBlobClientFactory';
import { HttpRequest } from '../http/HttpRequest';
import { isDefined } from '../utils/nonNull';

Expand Down Expand Up @@ -30,6 +31,9 @@ export function fromRpcTypedData(data: RpcTypedData | null | undefined): unknown
return data.collectionDouble.double;
} else if (data.collectionSint64 && isDefined(data.collectionSint64.sint64)) {
return data.collectionSint64.sint64;
} else if (data.modelBindingData && isDefined(data.modelBindingData.content)) {
return AzureStorageBlobClientFactory.buildClientFromModelBindingData(data.modelBindingData);
//return data.modelBindingData;
} else {
return undefined;
}
Expand Down
13 changes: 12 additions & 1 deletion src/converters/toCoreFunctionMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import { toRpcDuration } from './toRpcDuration';
export function toCoreFunctionMetadata(name: string, options: GenericFunctionOptions): coreTypes.FunctionMetadata {
const bindings: Record<string, coreTypes.RpcBindingInfo> = {};
const bindingNames: string[] = [];

const trigger = options.trigger;

bindings[trigger.name] = {
...trigger,
direction: 'in',
type: isTrigger(trigger.type) ? trigger.type : trigger.type + 'Trigger',
properties: addDeferredBindingsFlag(options.trigger?.deferredBinding),
};
bindingNames.push(trigger.name);

Expand All @@ -25,6 +26,7 @@ export function toCoreFunctionMetadata(name: string, options: GenericFunctionOpt
bindings[input.name] = {
...input,
direction: 'in',
properties: addDeferredBindingsFlag(input?.deferredBinding),
};
bindingNames.push(input.name);
}
Expand Down Expand Up @@ -74,3 +76,12 @@ export function toCoreFunctionMetadata(name: string, options: GenericFunctionOpt

return { name, bindings, retryOptions };
}

function addDeferredBindingsFlag(deferredBindingType?: boolean | unknown): { [key: string]: string } {
//Ensure that trigger type that is passed is valid and supported.
if (deferredBindingType !== undefined && deferredBindingType === true) {
return { supportsDeferredBinding: 'true' };
}

return { supportsDeferredBinding: 'false' };
}
90 changes: 90 additions & 0 deletions src/deferred-binding/connectionDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { ModelBindingData } from '@azure/functions-core';

export type BlobConnectionDetails = {
Connection: string;
ContainerName: string;
BlobName: string;
};

// Define the `ServiceBusConnectionInfo` type that extends `ConnectionInfo`
//TODO Define other connectionInfo example ServiceBusConnectionDetails

/**
* Type Guard to check if an object is of type BlobConnectionInfo
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
function isBlobConnectionDetails(obj: unknown): obj is BlobConnectionDetails {
return (
obj !== null &&
typeof obj === 'object' &&
'Connection' in obj &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof (obj as any).Connection === 'string' &&
'ContainerName' in obj &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof (obj as any).ContainerName === 'string' &&
'BlobName' in obj &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
typeof (obj as any).BlobName === 'string'
);
}

/**
* Function to parse JSON and determine its type
* @param jsonBuffer Bufer that holds the JSON string to parse
* @returns Either `BlobConnectionDetails` or `ServiceBusConnectionDetails`
*/
export function parseConnectionDetails(jsonBuffer: Buffer | null | undefined): BlobConnectionDetails {
if (jsonBuffer === null || jsonBuffer === undefined) {
throw new Error('Connection details content is null or undefined');
}
const parsedObject: unknown = JSON.parse(jsonBuffer.toString());

if (isBlobConnectionDetails(parsedObject)) {
return parsedObject;
}
//TODO add other parser for different resource types
else {
throw new Error('Invalid connection info type');
}
}

/**
* Type guard to check if an object conforms to the ModelBindingData interface
* @param obj Object to check
* @returns True if object is ModelBindingData
*/
export function isModelBindingData(obj: unknown): obj is ModelBindingData {
if (!obj || typeof obj !== 'object') {
return false;
}

const candidate = obj as Record<string, unknown>;

// Check content property if it exists
if (
'content' in candidate &&
candidate.content !== null &&
candidate.content !== undefined &&
candidate.content instanceof Buffer
) {
return false;
}

// Check string properties if they exist
const stringProps = ['contentType', 'source', 'version'];
for (const prop of stringProps) {
if (
prop in candidate &&
candidate[prop] !== null &&
candidate[prop] !== undefined &&
typeof candidate[prop] !== 'string'
) {
return false;
}
}
return true;
}
56 changes: 56 additions & 0 deletions src/deferred-binding/storage-blob/azureStorageBlobClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { BlobClient, ContainerClient, StoragePipelineOptions } from '@azure/storage-blob';
import { StorageBlobServiceClientStrategy } from './storageBlobServiceClientStrategy';

export class AzureStorageBlobClient {
private blobClient: BlobClient | undefined;
private containerClient: ContainerClient | undefined;

/**
* Creates a new AzureStorageBlobClient instance
*
* @param strategyOrAccountUrl - The strategy to use for creating the BlobServiceClient or the account URL
* @param credentialOrOptions - The credential to use for authentication or storage pipeline options
* @param options - Storage pipeline options (optional, only used when the first parameter is an account URL)
*/
constructor(
strategy: StorageBlobServiceClientStrategy,
containerName?: string,
blobName?: string,
options?: StoragePipelineOptions
) {
const storageBlobServiceClient = strategy.createStorageBlobServiceClient(options);
// Initialize container and blob clients if names are provided
if (containerName) {
this.containerClient = storageBlobServiceClient.getContainerClient(containerName);

if (blobName) {
this.blobClient = this.containerClient.getBlobClient(blobName);
}
}
}

/**
* Gets the BlobClient instance
* @returns The BlobClient
*/
getBlobClient(): BlobClient {
if (!this.blobClient) {
throw new Error('No blob client available. A blob name must be provided in the constructor.');
}
return this.blobClient;
}

/**
* Gets the ContainerClient instance
* @returns The ContainerClient
*/
getContainerClient(): ContainerClient {
if (!this.containerClient) {
throw new Error('No container client available. A container name must be provided in the constructor.');
}
return this.containerClient;
}
}
78 changes: 78 additions & 0 deletions src/deferred-binding/storage-blob/azureStorageBlobClientFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { ModelBindingData } from '@azure/functions-core';
import { StorageBlobClient } from '../../../types/storageBlobClient';
import { BlobConnectionDetails, parseConnectionDetails } from '../connectionDetails';
import { AzureStorageBlobClient } from './azureStorageBlobClient';
import { ConnectionStringStrategy } from './connectionStringStrategy';
import { ManagedIdentitySystemStrategy } from './managedIdentitySystemStrategy';
import { ManagedIdentityUserStrategy } from './managedIdentityUserStartegy';
import { StorageBlobServiceClientStrategy } from './storageBlobServiceClientStrategy';
import { getConnectionString, isSystemBasedManagedIdentity, isUserBasedManagedIdentity } from './utils';

/**
* Factory class for creating Azure Blob Storage clients
*/
export class AzureStorageBlobClientFactory {
static buildClientFromModelBindingData(modelBindingData: ModelBindingData): StorageBlobClient {
const connectionDetails = parseConnectionDetails(modelBindingData.content);
//TODO Add type check and parsing for other connection types
return this.fromConnectionDetailsToBlobStorageClient(connectionDetails);
}

/**
* Creates a StorageBlobClient directly from connection parameters
*
* @param connectionDetails - Connection details for the blob storage client
* @returns - StorageBlobClient object containing the blob client and container client
*/
static fromConnectionDetailsToBlobStorageClient(connectionDetails: BlobConnectionDetails): StorageBlobClient {
try {
const connectionName: string = connectionDetails.Connection;
const connectionUrl = getConnectionString(connectionName);
const connectionStrategy = this.createConnectionStrategy(connectionName, connectionUrl);

const azureStorageBlobClient = new AzureStorageBlobClient(
connectionStrategy,
connectionDetails.ContainerName,
connectionDetails.BlobName
);

const storageBlobClient: StorageBlobClient = {
blobClient: azureStorageBlobClient.getBlobClient(),
conatinerClient: azureStorageBlobClient.getContainerClient(),
};
return storageBlobClient;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to create client from parameters: ${errorMessage}`);
}
}

/**
* Creates the appropriate connection strategy based on the connection name and URL
*
* @param connectionName - The connection name
* @param connectionUrl - The resolved connection URL
* @returns The appropriate StorageBlobServiceClientStrategy
*/
static createConnectionStrategy(connectionName: string, connectionUrl: string): StorageBlobServiceClientStrategy {
// User-assigned managed identity takes precedence
if (isUserBasedManagedIdentity(connectionName)) {
const clientId = process.env[`${connectionName}__clientId`];
if (!clientId) {
throw new Error(`Environment variable ${connectionName}__clientId is not defined.`);
}
return new ManagedIdentityUserStrategy(connectionUrl, clientId);
}

// Next, check for system-assigned managed identity
if (isSystemBasedManagedIdentity(connectionName)) {
return new ManagedIdentitySystemStrategy(connectionUrl);
}

// Default to connection string
return new ConnectionStringStrategy(connectionUrl);
}
}
19 changes: 19 additions & 0 deletions src/deferred-binding/storage-blob/connectionStringStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { BlobServiceClient, StoragePipelineOptions } from '@azure/storage-blob';
import { StorageBlobServiceClientStrategy } from './storageBlobServiceClientStrategy';

/**
* Strategy for creating BlobServiceClient using connection string
*/
export class ConnectionStringStrategy implements StorageBlobServiceClientStrategy {
/**
* @param connectionString - Azure Storage connection string
*/
constructor(private connectionString: string) {}

createStorageBlobServiceClient(options?: StoragePipelineOptions): BlobServiceClient {
return BlobServiceClient.fromConnectionString(this.connectionString, options);
}
}
Loading
Loading