feat(integration): Add AWS CodeCommit URL Reader/Integration

Signed-off-by: Stijn Brouwers (EISMEA) <stijn@bdcommit.com>
This commit is contained in:
Stijn Brouwers (EISMEA)
2024-03-24 09:03:34 +01:00
parent d5cf11e7d7
commit 7b1142251c
21 changed files with 1532 additions and 0 deletions
+46
View File
@@ -7,6 +7,36 @@ import { Config } from '@backstage/config';
import { ConsumedResponse } from '@backstage/errors';
import { RestEndpointMethodTypes } from '@octokit/rest';
// @public
export class AwsCodeCommitIntegration implements ScmIntegration {
constructor(integrationConfig: AwsCodeCommitIntegrationConfig);
// (undocumented)
get config(): AwsCodeCommitIntegrationConfig;
// (undocumented)
static factory: ScmIntegrationsFactory<AwsCodeCommitIntegration>;
// (undocumented)
resolveEditUrl(url: string): string;
// (undocumented)
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number | undefined;
}): string;
// (undocumented)
get title(): string;
// (undocumented)
get type(): string;
}
// @public
export type AwsCodeCommitIntegrationConfig = {
host: string;
accessKeyId?: string;
secretAccessKey?: string;
roleArn?: string;
externalId?: string;
};
// @public
export class AwsS3Integration implements ScmIntegration {
constructor(integrationConfig: AwsS3IntegrationConfig);
@@ -646,6 +676,8 @@ export type GoogleGcsIntegrationConfig = {
// @public
export interface IntegrationsByType {
// (undocumented)
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
// (undocumented)
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
// (undocumented)
@@ -703,6 +735,16 @@ export interface RateLimitInfo {
isRateLimited: boolean;
}
// @public
export function readAwsCodeCommitIntegrationConfig(
config: Config,
): AwsCodeCommitIntegrationConfig;
// @public
export function readAwsCodeCommitIntegrationConfigs(
configs: Config[],
): AwsCodeCommitIntegrationConfig[];
// @public
export function readAwsS3IntegrationConfig(
config: Config,
@@ -828,6 +870,8 @@ export interface ScmIntegration {
export interface ScmIntegrationRegistry
extends ScmIntegrationsGroup<ScmIntegration> {
// (undocumented)
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
// (undocumented)
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
// (undocumented)
azure: ScmIntegrationsGroup<AzureIntegration>;
@@ -857,6 +901,8 @@ export interface ScmIntegrationRegistry
export class ScmIntegrations implements ScmIntegrationRegistry {
constructor(integrationsByType: IntegrationsByType);
// (undocumented)
get awsCodeCommit(): ScmIntegrationsGroup<AwsCodeCommitIntegration>;
// (undocumented)
get awsS3(): ScmIntegrationsGroup<AwsS3Integration>;
// (undocumented)
get azure(): ScmIntegrationsGroup<AzureIntegration>;
@@ -36,12 +36,18 @@ import { GitLabIntegration } from './gitlab/GitLabIntegration';
import { basicIntegrations } from './helpers';
import { ScmIntegrations } from './ScmIntegrations';
import { GiteaIntegration, GiteaIntegrationConfig } from './gitea';
import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration';
import { AwsCodeCommitIntegrationConfig } from './awsCodeCommit';
describe('ScmIntegrations', () => {
const awsS3 = new AwsS3Integration({
host: 'awss3.local',
} as AwsS3IntegrationConfig);
const awsCodeCommit = new AwsCodeCommitIntegration({
host: 'awscodecommit.local',
} as AwsCodeCommitIntegrationConfig);
const azure = new AzureIntegration({
host: 'azure.local',
} as AzureIntegrationConfig);
@@ -76,6 +82,7 @@ describe('ScmIntegrations', () => {
const i = new ScmIntegrations({
awsS3: basicIntegrations([awsS3], item => item.config.host),
awsCodeCommit: basicIntegrations([awsCodeCommit], item => item.config.host),
azure: basicIntegrations([azure], item => item.config.host),
bitbucket: basicIntegrations([bitbucket], item => item.config.host),
bitbucketCloud: basicIntegrations([bitbucketCloud], item => item.title),
@@ -91,6 +98,9 @@ describe('ScmIntegrations', () => {
it('can get the specifics', () => {
expect(i.awsS3.byUrl('https://awss3.local')).toBe(awsS3);
expect(i.awsCodeCommit.byUrl('https://awscodecommit.local')).toBe(
awsCodeCommit,
);
expect(i.azure.byUrl('https://azure.local')).toBe(azure);
expect(i.bitbucket.byUrl('https://bitbucket.local')).toBe(bitbucket);
expect(i.bitbucketCloud.byUrl('https://bitbucket.org')).toBe(
@@ -109,6 +119,7 @@ describe('ScmIntegrations', () => {
expect(i.list()).toEqual(
expect.arrayContaining([
awsS3,
awsCodeCommit,
azure,
bitbucket,
bitbucketCloud,
@@ -123,6 +134,7 @@ describe('ScmIntegrations', () => {
it('can select by url and host', () => {
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.byUrl('https://bitbucket.local')).toBe(bitbucket);
expect(i.byUrl('https://bitbucket.org')).toBe(bitbucketCloud);
@@ -133,6 +145,7 @@ describe('ScmIntegrations', () => {
expect(i.byUrl('https://gitea.local')).toBe(gitea);
expect(i.byHost('awss3.local')).toBe(awsS3);
expect(i.byHost('awscodecommit.local')).toBe(awsCodeCommit);
expect(i.byHost('azure.local')).toBe(azure);
expect(i.byHost('bitbucket.local')).toBe(bitbucket);
expect(i.byHost('bitbucket.org')).toBe(bitbucketCloud);
@@ -16,6 +16,7 @@
import { Config } from '@backstage/config';
import { AwsS3Integration } from './awsS3/AwsS3Integration';
import { AwsCodeCommitIntegration } from './awsCodeCommit/AwsCodeCommitIntegration';
import { AzureIntegration } from './azure/AzureIntegration';
import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration';
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
@@ -35,6 +36,7 @@ import { GiteaIntegration } from './gitea';
*/
export interface IntegrationsByType {
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
azure: ScmIntegrationsGroup<AzureIntegration>;
/**
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`
@@ -59,6 +61,7 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
static fromConfig(config: Config): ScmIntegrations {
return new ScmIntegrations({
awsS3: AwsS3Integration.factory({ config }),
awsCodeCommit: AwsCodeCommitIntegration.factory({ config }),
azure: AzureIntegration.factory({ config }),
bitbucket: BitbucketIntegration.factory({ config }),
bitbucketCloud: BitbucketCloudIntegration.factory({ config }),
@@ -78,6 +81,10 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
return this.byType.awsS3;
}
get awsCodeCommit(): ScmIntegrationsGroup<AwsCodeCommitIntegration> {
return this.byType.awsCodeCommit;
}
get azure(): ScmIntegrationsGroup<AzureIntegration> {
return this.byType.azure;
}
@@ -0,0 +1,109 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import { AwsCodeCommitIntegration } from './AwsCodeCommitIntegration';
const AMAZON_AWS_CODECOMMIT_HOST = 'console.aws.amazon.com';
describe('AwsCodeCommitIntegration', () => {
it('has a working factory with correct data when entry provided', () => {
const integrations = AwsCodeCommitIntegration.factory({
config: new ConfigReader({
integrations: {
awsCodeCommit: [
{
accessKeyId: 'access key',
secretAccessKey: ' secret key ',
roleArn: `role arn`,
externalId: `external id`,
},
],
},
}),
});
expect(integrations.list().length).toBe(1); // including default
expect(integrations.list()[0].config.host).toBe(AMAZON_AWS_CODECOMMIT_HOST);
expect(integrations.list()[0].config.accessKeyId).toBe('access key');
expect(integrations.list()[0].config.secretAccessKey).toBe('secret key');
expect(integrations.list()[0].config.roleArn).toBe(`role arn`);
expect(integrations.list()[0].config.externalId).toBe(`external id`);
});
it('has a working factory with default values when no data provided', () => {
const integrations = AwsCodeCommitIntegration.factory({
config: new ConfigReader({
integrations: {},
}),
});
expect(integrations.list().length).toBe(1); // including default
expect(integrations.list()[0].config.host).toBe(AMAZON_AWS_CODECOMMIT_HOST);
expect(integrations.list()[0].config.accessKeyId).toBeUndefined();
expect(integrations.list()[0].config.secretAccessKey).toBeUndefined();
expect(integrations.list()[0].config.roleArn).toBeUndefined();
expect(integrations.list()[0].config.externalId).toBeUndefined();
});
it('returns the basics', () => {
const integration = new AwsCodeCommitIntegration({ host: 'a.com' } as any);
expect(integration.type).toBe('awsCodeCommit');
expect(integration.title).toBe('a.com');
});
describe('resolveUrl', () => {
it('works for valid urls', () => {
const integration = new AwsCodeCommitIntegration({
host: 'amazonaws.com',
} as any);
expect(
integration.resolveUrl({
url: 'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/refs/heads/main/--/catalog-info.yaml',
base: 'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/refs/heads/main/--/catalog-info.yaml',
}),
).toBe(
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/refs/heads/main/--/catalog-info.yaml',
);
});
});
it('resolve edit URL - with refs', () => {
const integration = new AwsCodeCommitIntegration({
host: 'console.aws.amazon.com',
} as any);
expect(
integration.resolveEditUrl(
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/refs/heads/main/--/catalog-info.yaml?region=eu-west-1',
),
).toBe(
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/files/edit/refs/heads/main/--/catalog-info.yaml?region=eu-west-1',
);
});
it('resolve edit URL - without refs', () => {
const integration = new AwsCodeCommitIntegration({
host: 'console.aws.amazon.com',
} as any);
expect(
integration.resolveEditUrl(
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml?region=eu-west-1',
),
).toBe(
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/files/edit/--/catalog-info.yaml?region=eu-west-1',
);
});
});
@@ -0,0 +1,98 @@
/*
* 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 { basicIntegrations, defaultScmResolveUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import {
AwsCodeCommitIntegrationConfig,
readAwsCodeCommitIntegrationConfigs,
} from './config';
/**
* Integrates with AWS CodeCommit.
*
* @public
*/
export class AwsCodeCommitIntegration implements ScmIntegration {
static factory: ScmIntegrationsFactory<AwsCodeCommitIntegration> = ({
config,
}) => {
const configs = readAwsCodeCommitIntegrationConfigs(
config.getOptionalConfigArray('integrations.awsCodeCommit') ?? [],
);
return basicIntegrations(
configs.map(c => new AwsCodeCommitIntegration(c)),
i => i.config.host,
);
};
get type(): string {
return 'awsCodeCommit';
}
get config(): AwsCodeCommitIntegrationConfig {
return this.integrationConfig;
}
get title(): string {
return this.integrationConfig.host;
}
constructor(
private readonly integrationConfig: AwsCodeCommitIntegrationConfig,
) {}
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number | undefined;
}): string {
const resolved = defaultScmResolveUrl(options);
return resolved;
}
resolveEditUrl(url: string): string {
const parsedUrl = new URL(url);
const pathMatch = parsedUrl.pathname.match(
/^\/codesuite\/codecommit\/repositories\/([^\/]+)\//,
);
if (!pathMatch) {
throw new Error(``);
}
const [, repositoryName] = pathMatch;
return replaceCodeCommitUrlType(url, repositoryName, 'edit');
}
}
/**
* Takes a CodeCommit URL and replaces the type part (blob, tree etc).
*
* @param url - The original URL
* @param type - The desired type, e.g. 'blob', 'edit'
* @public
*/
export function replaceCodeCommitUrlType(
url: string,
repositoryName: string,
type: 'browse' | 'edit',
): string {
const newString = type === 'edit' ? `files/edit` : type;
return url.replace(
new RegExp(
`\/codesuite\/codecommit\/repositories\/${repositoryName}\/(browse|files\/edit)\/`,
),
`/codesuite/codecommit/repositories/${repositoryName}/${newString}/`,
);
}
@@ -0,0 +1,93 @@
/*
* 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, ConfigReader } from '@backstage/config';
import {
AwsCodeCommitIntegrationConfig,
readAwsCodeCommitIntegrationConfig,
readAwsCodeCommitIntegrationConfigs,
} from './config';
const AMAZON_AWS_CODECOMMIT_HOST = 'console.aws.amazon.com';
describe('readAwsCodeCommitIntegrationConfig', () => {
function buildConfig(data: Partial<AwsCodeCommitIntegrationConfig>): Config {
return new ConfigReader(data);
}
it('reads all values (default)', () => {
const output = readAwsCodeCommitIntegrationConfig(buildConfig({}));
expect(output).toEqual({
host: AMAZON_AWS_CODECOMMIT_HOST,
});
});
it('reads all values (custom entry)', () => {
const output = readAwsCodeCommitIntegrationConfig(
buildConfig({
accessKeyId: 'fake-key',
secretAccessKey: 'fake-secret-key',
externalId: 'fake-external-id',
roleArn: 'fake-role-arn',
}),
);
expect(output).toEqual({
host: AMAZON_AWS_CODECOMMIT_HOST,
accessKeyId: 'fake-key',
secretAccessKey: 'fake-secret-key',
externalId: 'fake-external-id',
roleArn: 'fake-role-arn',
});
});
});
describe('readAwsCodeCommitIntegrationConfigs', () => {
function buildConfig(
data: Partial<AwsCodeCommitIntegrationConfig>[],
): Config[] {
return data.map(item => new ConfigReader(item));
}
it('reads all values', () => {
const output = readAwsCodeCommitIntegrationConfigs(
buildConfig([
{
host: AMAZON_AWS_CODECOMMIT_HOST,
accessKeyId: 'key',
secretAccessKey: 'secret',
externalId: 'external-id',
roleArn: 'role-arn',
},
]),
);
expect(output).toContainEqual({
host: AMAZON_AWS_CODECOMMIT_HOST,
accessKeyId: 'key',
secretAccessKey: 'secret',
externalId: 'external-id',
roleArn: 'role-arn',
});
});
it('adds a default entry when missing', () => {
const output = readAwsCodeCommitIntegrationConfigs(buildConfig([]));
expect(output).toEqual([
{
host: AMAZON_AWS_CODECOMMIT_HOST,
},
]);
});
});
@@ -0,0 +1,100 @@
/*
* 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 AMAZON_AWS_CODECOMMIT_HOST = 'console.aws.amazon.com';
/**
* The configuration parameters for a single AWS CodeCommit provider.
*
* @public
*/
export type AwsCodeCommitIntegrationConfig = {
/**
* Host, git host derived from region
*/
host: string;
/**
* (Optional) User access key id
*/
accessKeyId?: string;
/**
* (Optional) User secret access key
*/
secretAccessKey?: string;
/**
* (Optional) ARN of role to be assumed
*/
roleArn?: string;
/**
* (Optional) External ID to use when assuming role
*/
externalId?: string;
};
/**
* Reads a single Aws CodeCommit integration config.
*
* @param config - The config object of a single integration
* @public
*/
export function readAwsCodeCommitIntegrationConfig(
config: Config,
): AwsCodeCommitIntegrationConfig {
const accessKeyId = config.getOptionalString('accessKeyId');
const secretAccessKey = config.getOptionalString('secretAccessKey')?.trim();
const roleArn = config.getOptionalString('roleArn');
const externalId = config.getOptionalString('externalId');
const host = AMAZON_AWS_CODECOMMIT_HOST;
return {
host,
accessKeyId,
secretAccessKey,
roleArn,
externalId,
};
}
/**
* Reads a set of AWS CodeCommit integration configs, and inserts some defaults for
* public Amazon AWS if not specified.
*
* @param configs - The config objects of the integrations
* @public
*/
export function readAwsCodeCommitIntegrationConfigs(
configs: Config[],
): AwsCodeCommitIntegrationConfig[] {
// First read all the explicit integrations
const result = configs.map(readAwsCodeCommitIntegrationConfig);
// If no explicit console.aws.amazon.com integration was added, put one in the list as
// a convenience
if (!result.some(c => c.host === AMAZON_AWS_CODECOMMIT_HOST)) {
result.push({
host: AMAZON_AWS_CODECOMMIT_HOST,
});
}
return result;
}
@@ -0,0 +1,21 @@
/*
* 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 { AwsCodeCommitIntegration } from './AwsCodeCommitIntegration';
export {
readAwsCodeCommitIntegrationConfig,
readAwsCodeCommitIntegrationConfigs,
} from './config';
export type { AwsCodeCommitIntegrationConfig } from './config';
+1
View File
@@ -21,6 +21,7 @@
*/
export * from './awsS3';
export * from './awsCodeCommit';
export * from './azure';
export * from './bitbucket';
export * from './bitbucketCloud';
+2
View File
@@ -16,6 +16,7 @@
import { ScmIntegration, ScmIntegrationsGroup } from './types';
import { AwsS3Integration } from './awsS3/AwsS3Integration';
import { AwsCodeCommitIntegration } from './awsCodeCommit';
import { AzureIntegration } from './azure/AzureIntegration';
import { BitbucketCloudIntegration } from './bitbucketCloud/BitbucketCloudIntegration';
import { BitbucketIntegration } from './bitbucket/BitbucketIntegration';
@@ -33,6 +34,7 @@ import { GiteaIntegration } from './gitea/GiteaIntegration';
export interface ScmIntegrationRegistry
extends ScmIntegrationsGroup<ScmIntegration> {
awsS3: ScmIntegrationsGroup<AwsS3Integration>;
awsCodeCommit: ScmIntegrationsGroup<AwsCodeCommitIntegration>;
azure: ScmIntegrationsGroup<AzureIntegration>;
/**
* @deprecated in favor of `bitbucketCloud` and `bitbucketServer`