feat: azure blob storage entity provider for catalog implemented
Signed-off-by: NIKUNJ LALITKUMAR HUDKA <nk856850@dal.ca>
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/identity": "^4.0.0",
|
||||
"@azure/storage-blob": "^12.5.0",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@octokit/auth-app": "^4.0.0",
|
||||
|
||||
@@ -39,6 +39,10 @@ import { GiteaIntegration, GiteaIntegrationConfig } from './gitea';
|
||||
import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration';
|
||||
import { AwsCodeCommitIntegrationConfig } from './awsCodeCommit';
|
||||
import { HarnessIntegration, HarnessIntegrationConfig } from './harness';
|
||||
import {
|
||||
AzureBlobStorageIntegrationConfig,
|
||||
AzureBlobStorageIntergation,
|
||||
} from './azureBlobStorage';
|
||||
|
||||
describe('ScmIntegrations', () => {
|
||||
const awsS3 = new AwsS3Integration({
|
||||
@@ -53,6 +57,10 @@ describe('ScmIntegrations', () => {
|
||||
host: 'azure.local',
|
||||
} as AzureIntegrationConfig);
|
||||
|
||||
const azureBlob = new AzureBlobStorageIntergation({
|
||||
host: 'azureblobstorage.local',
|
||||
} as AzureBlobStorageIntegrationConfig);
|
||||
|
||||
const bitbucket = new BitbucketIntegration({
|
||||
host: 'bitbucket.local',
|
||||
} as BitbucketIntegrationConfig);
|
||||
@@ -89,6 +97,7 @@ describe('ScmIntegrations', () => {
|
||||
awsS3: basicIntegrations([awsS3], item => item.config.host),
|
||||
awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host),
|
||||
azure: basicIntegrations([azure], item => item.config.host),
|
||||
azureBlobStorage: basicIntegrations([azureBlob], item => item.config.host),
|
||||
bitbucket: basicIntegrations([bitbucket], item => item.config.host),
|
||||
bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title),
|
||||
bitbucketServer: basicIntegrations(
|
||||
@@ -108,6 +117,9 @@ describe('ScmIntegrations', () => {
|
||||
awsCodeCommit,
|
||||
);
|
||||
expect(i.azure.byUrl('https://azure.local')).toBe(azure);
|
||||
expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe(
|
||||
azureBlob,
|
||||
);
|
||||
expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe(
|
||||
bitbucketCloud,
|
||||
@@ -128,6 +140,7 @@ describe('ScmIntegrations', () => {
|
||||
awsS3,
|
||||
awsCodeCommit,
|
||||
azure,
|
||||
azureBlob,
|
||||
bitbucket,
|
||||
bitbucketCloud,
|
||||
bitbucketServer,
|
||||
@@ -144,6 +157,9 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byUrl('https://awss3.local')).toBe(awsS3);
|
||||
expect(i.byUrl('https://awscodecommit.local')).toBe(awsCodeCommit);
|
||||
expect(i.byUrl('https://azure.local')).toBe(azure);
|
||||
expect(i.azureBlobStorage.byUrl('https://azureblobstorage.local')).toBe(
|
||||
azureBlob,
|
||||
);
|
||||
expect(i.byUrl('https://bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud);
|
||||
expect(i.byUrl('https://bitbucket-server.local')).toBe(bitbucketServer);
|
||||
@@ -156,6 +172,7 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('awss3.local')).toBe(awsS3);
|
||||
expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit);
|
||||
expect(i.byHost('azure.local')).toBe(azure);
|
||||
expect(i.byHost('azureblobstorage.local')).toBe(azureBlob);
|
||||
expect(i.byHost('bitbucket.local')).toBe(bitbucket);
|
||||
expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud);
|
||||
expect(i.byHost('bitbucket-server.local')).toBe(bitbucketServer);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ScmIntegration, ScmIntegrationsGroup } from './types';
|
||||
import { ScmIntegrationRegistry } from './registry';
|
||||
import { GiteaIntegration } from './gitea';
|
||||
import { HarnessIntegration } from './harness/HarnessIntegration';
|
||||
import { AzureBlobStorageIntergation } from './azureBlobStorage';
|
||||
|
||||
/**
|
||||
* The set of supported integrations.
|
||||
@@ -38,6 +39,7 @@ import { HarnessIntegration } from './harness/HarnessIntegration';
|
||||
export interface IntegrationsByType {
|
||||
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
|
||||
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
@@ -64,6 +66,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return new ScmIntegrations({
|
||||
awsS3: AwsS3Integration.factory({ config }),
|
||||
awsCodeCommit: AwsCodeCommitIntegration.factory({ config }),
|
||||
azureBlobStorage: AzureBlobStorageIntergation.factory({ config }),
|
||||
azure: AzureIntegration.factory({ config }),
|
||||
bitbucket: BitbucketIntegration.factory({ config }),
|
||||
bitbucketCloud: BitbucketCloudIntegration.factory({ config }),
|
||||
@@ -88,6 +91,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
return this.byType.awsCodeCommit;
|
||||
}
|
||||
|
||||
get azureBlobStorage(): ScmIntegrationsGroup<AzureBlobStorageIntergation> {
|
||||
return this.byType.azureBlobStorage;
|
||||
}
|
||||
|
||||
get azure(): ScmIntegrationsGroup<AzureIntegration> {
|
||||
return this.byType.azure;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { AzureBlobStorageIntergation } from './AzureBlobStorageIntegration';
|
||||
|
||||
describe('AzureBlobStorageIntegration', () => {
|
||||
it('has a working factory', () => {
|
||||
const integrations = AzureBlobStorageIntergation.factory({
|
||||
config: new ConfigReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
endpoint: 'https://myaccount.blob.core.windows.net',
|
||||
accountName: 'myaccount',
|
||||
accountKey: 'someAccountKey',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(integrations.list().length).toBe(2); // including default
|
||||
expect(integrations.list()[0].config.host).toBe(
|
||||
'myaccount.blob.core.windows.net',
|
||||
);
|
||||
expect(integrations.list()[1].config.host).toBe('blob.core.windows.net'); // default integration
|
||||
});
|
||||
|
||||
it('returns the basics', () => {
|
||||
const integration = new AzureBlobStorageIntergation({
|
||||
host: 'myaccount.blob.core.windows.net',
|
||||
} as any);
|
||||
expect(integration.type).toBe('azureBlobStorage');
|
||||
expect(integration.title).toBe('myaccount.blob.core.windows.net');
|
||||
});
|
||||
|
||||
describe('resolveUrl', () => {
|
||||
it('works for valid URLs', () => {
|
||||
const integration = new AzureBlobStorageIntergation({
|
||||
host: 'blob.core.windows.net',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://myaccount.blob.core.windows.net/container/file.yaml',
|
||||
base: 'https://myaccount.blob.core.windows.net/container/file.yaml',
|
||||
}),
|
||||
).toBe('https://myaccount.blob.core.windows.net/container/file.yaml');
|
||||
});
|
||||
});
|
||||
|
||||
it('resolve edit URL', () => {
|
||||
const integration = new AzureBlobStorageIntergation({
|
||||
host: 'myaccount.blob.core.windows.net',
|
||||
} as any);
|
||||
|
||||
// TODO: The Azure Blob Storage integration doesn't support resolving an edit URL,
|
||||
// instead we keep the input URL.
|
||||
expect(
|
||||
integration.resolveEditUrl(
|
||||
'https://myaccount.blob.core.windows.net/container/file.yaml',
|
||||
),
|
||||
).toBe('https://myaccount.blob.core.windows.net/container/file.yaml');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { basicIntegrations, defaultScmResolveUrl } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import {
|
||||
AzureBlobStorageIntegrationConfig,
|
||||
readAzureBlobStorageIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
export class AzureBlobStorageIntergation implements ScmIntegration {
|
||||
static factory: ScmIntegrationsFactory<AzureBlobStorageIntergation> = ({
|
||||
config,
|
||||
}) => {
|
||||
const configs = readAzureBlobStorageIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.azureBlobStorage') ?? [],
|
||||
);
|
||||
return basicIntegrations(
|
||||
configs.map(c => new AzureBlobStorageIntergation(c)),
|
||||
i => i.config.host,
|
||||
);
|
||||
};
|
||||
|
||||
get type(): string {
|
||||
return 'azureBlobStorage';
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this.integrationConfig.host;
|
||||
}
|
||||
|
||||
get config(): AzureBlobStorageIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly integrationConfig: AzureBlobStorageIntegrationConfig,
|
||||
) {}
|
||||
|
||||
resolveUrl(options: {
|
||||
url: string;
|
||||
base: string;
|
||||
lineNumber?: number | undefined;
|
||||
}): string {
|
||||
const resolved = defaultScmResolveUrl(options);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
resolveEditUrl(url: string): string {
|
||||
// TODO: Implement edit URL for azureBlobStorage
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AccessToken,
|
||||
ClientSecretCredential,
|
||||
DefaultAzureCredential,
|
||||
TokenCredential,
|
||||
} from '@azure/identity';
|
||||
import { AzureBlobStorageIntegrationConfig } from './config';
|
||||
import { ScmIntegrationRegistry } from '../registry';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { DefaultAzureCredentialsManager } from './DefaultAzureCredentialsProvider';
|
||||
import { ScmIntegrations } from '../ScmIntegrations';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass<
|
||||
typeof ClientSecretCredential
|
||||
>;
|
||||
|
||||
jest.mock('@azure/identity');
|
||||
|
||||
describe('DefaultAzureCredentialsManager', () => {
|
||||
let mockIntegration: ScmIntegrationRegistry;
|
||||
|
||||
const buildProvider = (azureIntegrations: any[]) =>
|
||||
DefaultAzureCredentialsManager.fromIntegrations(
|
||||
ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
azureBlobStorage: azureIntegrations,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockIntegration = {
|
||||
azureBlobStorage: {
|
||||
list: jest.fn().mockReturnValue([
|
||||
{
|
||||
config: {
|
||||
accountName: 'testaccount',
|
||||
aadCredential: {
|
||||
clientId: 'someClientId',
|
||||
tenantId: 'someTenantId',
|
||||
clientSecret: 'someClientSecret',
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
} as unknown as ScmIntegrationRegistry;
|
||||
|
||||
MockedClientSecretCredential.prototype.getToken.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(),
|
||||
token: 'fake-client-secret-token',
|
||||
} as AccessToken),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create an instance from ScmIntegrationRegistry', () => {
|
||||
const manager =
|
||||
DefaultAzureCredentialsManager.fromIntegrations(mockIntegration);
|
||||
expect(manager).toBeInstanceOf(DefaultAzureCredentialsManager);
|
||||
});
|
||||
|
||||
it('should return cached credentials if available', async () => {
|
||||
const manager = buildProvider([
|
||||
{
|
||||
accountName: 'testaccount',
|
||||
aadCredential: {
|
||||
clientId: 'someClientId',
|
||||
tenantId: 'someTenantId',
|
||||
clientSecret: 'someClientSecret',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const mockCredential = new MockedClientSecretCredential(
|
||||
'someTenantId',
|
||||
'someClientId',
|
||||
'someClientSecret',
|
||||
);
|
||||
|
||||
const credential = await manager.getCredentials('testaccount');
|
||||
|
||||
const scopes = ['https://storage.azure.com/.default'];
|
||||
|
||||
const expectedToken = await mockCredential.getToken(scopes);
|
||||
const receivedToken = await credential.getToken(scopes);
|
||||
|
||||
expect(receivedToken?.token).toEqual(expectedToken.token);
|
||||
});
|
||||
|
||||
it('should use Azure AD credentials if aadCredential is provided', async () => {
|
||||
const manager = buildProvider([
|
||||
{
|
||||
accountName: 'testaccount',
|
||||
aadCredential: {
|
||||
clientId: 'someClientId',
|
||||
tenantId: 'someTenantId',
|
||||
clientSecret: 'someClientSecret',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const credential = await manager.getCredentials('testaccount');
|
||||
|
||||
expect(credential).toBeInstanceOf(ClientSecretCredential);
|
||||
});
|
||||
|
||||
it('should use DefaultAzureCredential if no aadCredential is provided', async () => {
|
||||
const manager = buildProvider([
|
||||
{
|
||||
accountName: 'testaccount',
|
||||
},
|
||||
]);
|
||||
|
||||
const credential = await manager.getCredentials('testaccount');
|
||||
|
||||
expect(credential).toBeInstanceOf(DefaultAzureCredential);
|
||||
});
|
||||
|
||||
it('should cache credentials after first retrieval', async () => {
|
||||
const manager = buildProvider([
|
||||
{
|
||||
accountName: 'testaccount',
|
||||
},
|
||||
]);
|
||||
|
||||
const credential = await manager.getCredentials('testaccount');
|
||||
|
||||
const cachedCredential = await manager.getCredentials('testaccount');
|
||||
expect(cachedCredential).toBe(credential);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
DefaultAzureCredential,
|
||||
ClientSecretCredential,
|
||||
TokenCredential,
|
||||
} from '@azure/identity';
|
||||
import { AzureBlobStorageIntegrationConfig } from './config';
|
||||
import { AzureCredentialsManager } from './types';
|
||||
import { ScmIntegrationRegistry } from '../registry';
|
||||
|
||||
/**
|
||||
* Default Azure Credentials Manager to dynamically select and manage Azure credentials.
|
||||
* It supports Service Principal, Managed Identity, SAS Token, Connection String, Account Key, and Anonymous access.
|
||||
*/
|
||||
export class DefaultAzureCredentialsManager implements AzureCredentialsManager {
|
||||
private config: AzureBlobStorageIntegrationConfig;
|
||||
private cachedCredentials: Map<string, TokenCredential>;
|
||||
|
||||
constructor(config: AzureBlobStorageIntegrationConfig) {
|
||||
this.config = config;
|
||||
this.cachedCredentials = new Map<string, TokenCredential>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of DefaultAzureCredentialsManager from a Backstage Config.
|
||||
*/
|
||||
static fromIntegrations(
|
||||
integration: ScmIntegrationRegistry,
|
||||
): DefaultAzureCredentialsManager {
|
||||
const azureConfig = integration.azureBlobStorage.list().length
|
||||
? integration.azureBlobStorage.list()[0].config
|
||||
: { host: 'blob.core.windows.net' }; // Default to Azure Blob Storage host if no config found
|
||||
|
||||
return new DefaultAzureCredentialsManager(azureConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the appropriate credential method and returns credentials for BlobServiceClient.
|
||||
* Supports:
|
||||
* - Service Principal
|
||||
* - Managed Identity
|
||||
* - SAS Token
|
||||
* - Connection String
|
||||
* - Account Key
|
||||
* - Anonymous access
|
||||
*/
|
||||
async getCredentials(accountName: string): Promise<TokenCredential> {
|
||||
// Check if the credentials are already cached
|
||||
if (this.cachedCredentials.has(accountName)) {
|
||||
return this.cachedCredentials.get(accountName)!;
|
||||
}
|
||||
|
||||
let credential: TokenCredential;
|
||||
|
||||
// Check for SAS Token
|
||||
// if (this.config.sasToken) {
|
||||
// // console.log('Using SAS Token for Azure Blob Storage authentication');
|
||||
// // SAS Token does not return a credential but can be used directly in BlobServiceClient
|
||||
// // Here we can simply return undefined or keep a placeholder if needed
|
||||
// return this.config.sasToken; // Or return a string for the URL using the SAS token
|
||||
// }
|
||||
// // Check for Connection String
|
||||
// else if (this.config.connectionString) {
|
||||
// // console.log(
|
||||
// // 'Using Connection String for Azure Blob Storage authentication',
|
||||
// // );
|
||||
// // return undefined; // Connection string will also not return a specific credential object
|
||||
// }
|
||||
// Check for Account Key
|
||||
// if (this.config.accountKey) {
|
||||
// // console.log('Using Account Key for Azure Blob Storage authentication');
|
||||
// credential = new StorageSharedKeyCredential(
|
||||
// accountName,
|
||||
// this.config.accountKey,
|
||||
// );
|
||||
// }
|
||||
// Check for AAD credentials
|
||||
|
||||
if (
|
||||
this.config.aadCredential &&
|
||||
this.config.aadCredential.clientId &&
|
||||
this.config.aadCredential.clientSecret &&
|
||||
this.config.aadCredential.tenantId
|
||||
) {
|
||||
credential = new ClientSecretCredential(
|
||||
this.config.aadCredential.tenantId,
|
||||
this.config.aadCredential.clientId,
|
||||
this.config.aadCredential.clientSecret,
|
||||
);
|
||||
}
|
||||
// Check for Anonymous access
|
||||
// else if (this.config.anonymousAccess) {
|
||||
// console.log('Using Anonymous Credential for Azure Blob Storage access');
|
||||
// credential = new AnonymousCredential();
|
||||
// }
|
||||
// Fallback to Managed Identity
|
||||
else {
|
||||
credential = new DefaultAzureCredential();
|
||||
}
|
||||
|
||||
// Cache the credentials for future use
|
||||
this.cachedCredentials.set(accountName, credential);
|
||||
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
AzureBlobStorageIntegrationConfig,
|
||||
readAzureBlobStorageIntegrationConfig,
|
||||
readAzureBlobStorageIntegrationConfigs,
|
||||
} from './config';
|
||||
|
||||
describe('readAzureBlobStorageIntegrationConfig', () => {
|
||||
function buildConfig(
|
||||
data: Partial<AzureBlobStorageIntegrationConfig>,
|
||||
): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
it('reads valid configuration with accountKey', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfig(
|
||||
buildConfig({
|
||||
accountName: 'mystorageaccount',
|
||||
accountKey: 'someAccountKey',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'blob.core.windows.net',
|
||||
endpoint: undefined,
|
||||
accountName: 'mystorageaccount',
|
||||
accountKey: 'someAccountKey',
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('reads valid configuration with sasToken', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfig(
|
||||
buildConfig({
|
||||
endpoint: 'https://blob.core.windows.net',
|
||||
accountName: 'mystorageaccount',
|
||||
sasToken: 'someSASToken',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'blob.core.windows.net',
|
||||
endpoint: 'https://blob.core.windows.net',
|
||||
accountName: 'mystorageaccount',
|
||||
accountKey: undefined,
|
||||
sasToken: 'someSASToken',
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('reads valid configuration with Azure AD credentials', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfig(
|
||||
buildConfig({
|
||||
accountName: 'mystorageaccount',
|
||||
aadCredential: {
|
||||
clientId: 'someClientId',
|
||||
tenantId: 'someTenantId',
|
||||
clientSecret: 'someClientSecret',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'blob.core.windows.net',
|
||||
endpoint: undefined,
|
||||
accountName: 'mystorageaccount',
|
||||
accountKey: undefined,
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: {
|
||||
clientId: 'someClientId',
|
||||
tenantId: 'someTenantId',
|
||||
clientSecret: 'someClientSecret',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reads valid configuration with a custom endpoint', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfig(
|
||||
buildConfig({
|
||||
endpoint: 'https://custom.blob.core.windows.net',
|
||||
accountName: 'customaccount',
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
host: 'custom.blob.core.windows.net',
|
||||
endpoint: 'https://custom.blob.core.windows.net',
|
||||
accountName: 'customaccount',
|
||||
accountKey: undefined,
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error for invalid endpoint URL', () => {
|
||||
const config = buildConfig({
|
||||
endpoint: 'invalid-url',
|
||||
accountName: 'invalidaccount',
|
||||
});
|
||||
|
||||
expect(() => readAzureBlobStorageIntegrationConfig(config)).toThrow(
|
||||
`invalid azureBlobStorage integration config, endpoint 'invalid-url' is not a valid URL`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error if endpoint has a path', () => {
|
||||
const config = buildConfig({
|
||||
endpoint: 'https://blob.core.windows.net/path',
|
||||
accountName: 'accountwithpath',
|
||||
});
|
||||
|
||||
expect(() => readAzureBlobStorageIntegrationConfig(config)).toThrow(
|
||||
`invalid azureBlobStorage integration config, endpoints cannot contain path, got 'https://blob.core.windows.net/path'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error if both accountKey and sasToken are provided', () => {
|
||||
const config = buildConfig({
|
||||
accountName: 'mystorageaccount',
|
||||
accountKey: 'someAccountKey',
|
||||
sasToken: 'someSASToken',
|
||||
});
|
||||
|
||||
expect(() => readAzureBlobStorageIntegrationConfig(config)).toThrow(
|
||||
`Invalid Azure Blob Storage config for mystorageaccount: Both account key and SAS token cannot be used simultaneously.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error if both aadCredential and accountKey/sasToken are provided', () => {
|
||||
const config = buildConfig({
|
||||
accountName: 'mystorageaccount',
|
||||
accountKey: 'someAccountKey',
|
||||
aadCredential: {
|
||||
clientId: 'someClientId',
|
||||
tenantId: 'someTenantId',
|
||||
clientSecret: 'someClientSecret',
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => readAzureBlobStorageIntegrationConfig(config)).toThrow(
|
||||
`Invalid Azure Blob Storage config for mystorageaccount: Cannot use both Azure AD credentials and account keys/SAS tokens for the same account.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readAzureBlobStorageIntegrationConfigs', () => {
|
||||
function buildConfigs(
|
||||
data: Partial<AzureBlobStorageIntegrationConfig>[],
|
||||
): Config[] {
|
||||
return data.map(item => new ConfigReader(item));
|
||||
}
|
||||
|
||||
it('reads all provided configurations', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfigs(
|
||||
buildConfigs([
|
||||
{
|
||||
host: 'blob.core.windows.net',
|
||||
accountName: 'account1',
|
||||
accountKey: 'someAccountKey',
|
||||
},
|
||||
{
|
||||
endpoint: 'https://custom.blob.core.windows.net',
|
||||
accountName: 'account2',
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'blob.core.windows.net',
|
||||
endpoint: undefined,
|
||||
accountName: 'account1',
|
||||
accountKey: 'someAccountKey',
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
},
|
||||
{
|
||||
host: 'custom.blob.core.windows.net',
|
||||
endpoint: 'https://custom.blob.core.windows.net',
|
||||
accountName: 'account2',
|
||||
accountKey: undefined,
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds default integration for blob.core.windows.net when missing', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfigs(
|
||||
buildConfigs([
|
||||
{
|
||||
endpoint: 'https://custom.blob.core.windows.net',
|
||||
accountName: 'account2',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'custom.blob.core.windows.net',
|
||||
endpoint: 'https://custom.blob.core.windows.net',
|
||||
accountName: 'account2',
|
||||
accountKey: undefined,
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
},
|
||||
{
|
||||
host: 'blob.core.windows.net',
|
||||
endpoint: undefined,
|
||||
accountName: undefined,
|
||||
accountKey: undefined,
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not add default integration if blob.core.windows.net already exists', () => {
|
||||
const output = readAzureBlobStorageIntegrationConfigs(
|
||||
buildConfigs([
|
||||
{ host: 'blob.core.windows.net', accountName: 'account1' },
|
||||
]),
|
||||
);
|
||||
expect(output).toEqual([
|
||||
{
|
||||
host: 'blob.core.windows.net',
|
||||
endpoint: undefined,
|
||||
accountName: 'account1',
|
||||
accountKey: undefined,
|
||||
sasToken: undefined,
|
||||
connectionString: undefined,
|
||||
endpointSuffix: undefined,
|
||||
aadCredential: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
const AZURE_HOST = 'blob.core.windows.net';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single Azure Blob Storage account.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AzureBlobStorageIntegrationConfig = {
|
||||
/**
|
||||
* The name of the Azure Storage Account, e.g., "mystorageaccount".
|
||||
*/
|
||||
accountName?: string;
|
||||
|
||||
/**
|
||||
* The primary or secondary key for the Azure Storage Account.
|
||||
* Only required if connectionString or SAS token are not specified.
|
||||
*/
|
||||
accountKey?: string;
|
||||
|
||||
/**
|
||||
* A Shared Access Signature (SAS) token for limited access to resources.
|
||||
*/
|
||||
sasToken?: string;
|
||||
|
||||
/**
|
||||
* A full connection string for the Azure Storage Account.
|
||||
* This includes the account name, key, and endpoint details.
|
||||
*/
|
||||
connectionString?: string;
|
||||
|
||||
/**
|
||||
* Optional endpoint suffix for custom domains or sovereign clouds.
|
||||
* e.g., "core.windows.net" for public Azure or "core.usgovcloudapi.net" for US Government cloud.
|
||||
*/
|
||||
endpointSuffix?: string;
|
||||
|
||||
/**
|
||||
* The host of the target that this matches on, e.g., "blob.core.windows.net".
|
||||
*/
|
||||
host: string;
|
||||
|
||||
endpoint?: string;
|
||||
/**
|
||||
* Optional credential to use for Azure Active Directory authentication.
|
||||
*/
|
||||
aadCredential?: {
|
||||
/**
|
||||
* The client ID of the Azure AD application.
|
||||
*/
|
||||
clientId: string;
|
||||
|
||||
/**
|
||||
* The tenant ID for Azure AD.
|
||||
*/
|
||||
tenantId: string;
|
||||
|
||||
/**
|
||||
* The client secret for the Azure AD application.
|
||||
*/
|
||||
clientSecret: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a single Azure Blob Storage integration config.
|
||||
*
|
||||
* @param config - The config object of a single integration.
|
||||
* @public
|
||||
*/
|
||||
export function readAzureBlobStorageIntegrationConfig(
|
||||
config: Config,
|
||||
): AzureBlobStorageIntegrationConfig {
|
||||
const endpoint = config.getOptionalString('endpoint');
|
||||
const accountName = config.getString('accountName');
|
||||
const accountKey = config.getOptionalString('accountKey')?.trim();
|
||||
const sasToken = config.getOptionalString('sasToken')?.trim();
|
||||
const connectionString = config.getOptionalString('connectionString')?.trim();
|
||||
const endpointSuffix = config.getOptionalString('endpointSuffix')?.trim();
|
||||
|
||||
let host;
|
||||
let pathname;
|
||||
if (endpoint) {
|
||||
try {
|
||||
const url = new URL(endpoint);
|
||||
host = url.host;
|
||||
pathname = url.pathname;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`invalid azureBlobStorage integration config, endpoint '${endpoint}' is not a valid URL`,
|
||||
);
|
||||
}
|
||||
if (pathname !== '/') {
|
||||
throw new Error(
|
||||
`invalid azureBlobStorage integration config, endpoints cannot contain path, got '${endpoint}'`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
host = AZURE_HOST;
|
||||
}
|
||||
const aadCredential = config.has('aadCredential')
|
||||
? {
|
||||
clientId: config.getString('aadCredential.clientId'),
|
||||
tenantId: config.getString('aadCredential.tenantId'),
|
||||
clientSecret: config.getString('aadCredential.clientSecret')?.trim(),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (accountKey && sasToken) {
|
||||
throw new Error(
|
||||
`Invalid Azure Blob Storage config for ${accountName}: Both account key and SAS token cannot be used simultaneously.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (aadCredential && (accountKey || sasToken)) {
|
||||
throw new Error(
|
||||
`Invalid Azure Blob Storage config for ${accountName}: Cannot use both Azure AD credentials and account keys/SAS tokens for the same account.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
endpoint,
|
||||
accountName,
|
||||
accountKey,
|
||||
sasToken,
|
||||
connectionString,
|
||||
endpointSuffix,
|
||||
aadCredential,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a set of Azure Blob Storage integration configs.
|
||||
*
|
||||
* @param configs - All of the integration config objects.
|
||||
* @public
|
||||
*/
|
||||
export function readAzureBlobStorageIntegrationConfigs(
|
||||
configs: Config[],
|
||||
): AzureBlobStorageIntegrationConfig[] {
|
||||
// First read all the explicit integrations
|
||||
const result = configs.map(readAzureBlobStorageIntegrationConfig);
|
||||
|
||||
// If no explicit blob.core.windows.net integration was added, put one in the list as
|
||||
// a convenience
|
||||
if (!result.some(c => c.host === AZURE_HOST)) {
|
||||
result.push({
|
||||
host: AZURE_HOST,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { AzureBlobStorageIntergation } from './AzureBlobStorageIntegration';
|
||||
export {
|
||||
readAzureBlobStorageIntegrationConfig,
|
||||
readAzureBlobStorageIntegrationConfigs,
|
||||
} from './config';
|
||||
export type { AzureBlobStorageIntegrationConfig } from './config';
|
||||
export { DefaultAzureCredentialsManager } from './DefaultAzureCredentialsProvider';
|
||||
export type { AzureCredentialsManager } from './types';
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TokenCredential } from '@azure/identity';
|
||||
import {
|
||||
StorageSharedKeyCredential,
|
||||
AnonymousCredential,
|
||||
} from '@azure/storage-blob';
|
||||
|
||||
export interface AzureCredentialsManager {
|
||||
getCredentials(
|
||||
accountName: string,
|
||||
): Promise<
|
||||
TokenCredential | StorageSharedKeyCredential | AnonymousCredential
|
||||
>;
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
export * from './awsS3';
|
||||
export * from './awsCodeCommit';
|
||||
export * from './azureBlobStorage';
|
||||
export * from './azure';
|
||||
export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GithubIntegration } from './github/GithubIntegration';
|
||||
import { GitLabIntegration } from './gitlab/GitLabIntegration';
|
||||
import { GiteaIntegration } from './gitea/GiteaIntegration';
|
||||
import { HarnessIntegration } from './harness/HarnessIntegration';
|
||||
import { AzureBlobStorageIntergation } from './azureBlobStorage';
|
||||
|
||||
/**
|
||||
* Holds all registered SCM integrations, of all types.
|
||||
@@ -36,6 +37,7 @@ export interface ScmIntegrationRegistry
|
||||
extends ScmIntegrationsGroup<ScmIntegration> {
|
||||
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
|
||||
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
|
||||
azureBlobStorage: ScmIntegrationsGroup<AzureBlobStorageIntergation>;
|
||||
azure: ScmIntegrationsGroup<AzureIntegration>;
|
||||
/**
|
||||
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
|
||||
|
||||
Reference in New Issue
Block a user