feat(integration): Add AWS CodeCommit URL Reader/Integration
Signed-off-by: Stijn Brouwers (EISMEA) <stijn@bdcommit.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
'@backstage/integration': minor
|
||||
---
|
||||
|
||||
Add AWS CodeCommit URL Reader/Integration
|
||||
@@ -69,6 +69,7 @@ See [TechDocs Architecture](architecture.md) to get an overview of where the bel
|
||||
| GitLab | Yes ✅ |
|
||||
| GitLab Enterprise | Yes ✅ |
|
||||
| Gitea | Yes ✅ |
|
||||
| AWS CodeCommit | Yes ✅ |
|
||||
|
||||
### File storage providers
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
id: locations
|
||||
sidebar_label: Locations
|
||||
title: Amazon Web Services CodeCommit Locations
|
||||
# prettier-ignore
|
||||
description: Setting up an integration with Amazon Web Services CodeCommit
|
||||
---
|
||||
|
||||
The AWS CodeCommit integration supports loading catalog entities from CodeCommit Repositories.
|
||||
Entities can be added to
|
||||
[static catalog configuration](../../features/software-catalog/configuration.md),
|
||||
or registered with the
|
||||
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
|
||||
plugin.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this integration, add configuration to your `app-config.yaml`:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
awsCodeCommit:
|
||||
- accessKeyId: ${AWS_ACCESS_KEY_ID}
|
||||
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
|
||||
```
|
||||
|
||||
Then make sure the environment variables `AWS_ACCESS_KEY_ID` and
|
||||
`AWS_SECRET_ACCESS_KEY` are set when you run Backstage.
|
||||
|
||||
Users with multiple AWS accounts may want to use a role for CodeCommit that is
|
||||
in a different AWS account. Using the `roleArn` parameter as seen below, you can
|
||||
instruct the AWS CodeCommit reader to assume a role before accessing CodeCommit:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
awsCodeCommit:
|
||||
- accessKeyId: ${AWS_ACCESS_KEY_ID}
|
||||
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
|
||||
roleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/example-role'
|
||||
externalId: 'some-id' # optional
|
||||
```
|
||||
|
||||
When no entries are added, the AWS CodeCommit reader will add a default entry that uses the [standard credentials provider chain](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html).
|
||||
@@ -95,6 +95,8 @@ nav:
|
||||
- AWS S3:
|
||||
- Locations: 'integrations/aws-s3/locations.md'
|
||||
- Discovery: 'integrations/aws-s3/discovery.md'
|
||||
- AWS CodeCommit:
|
||||
- Locations: 'integrations/aws-codecommit/locations.md'
|
||||
- Azure:
|
||||
- Locations: 'integrations/azure/locations.md'
|
||||
- Discovery: 'integrations/azure/discovery.md'
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/abort-controller": "^3.347.0",
|
||||
"@aws-sdk/client-codecommit": "^3.350.0",
|
||||
"@aws-sdk/client-s3": "^3.350.0",
|
||||
"@aws-sdk/credential-providers": "^3.350.0",
|
||||
"@aws-sdk/types": "^3.347.0",
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
* Copyright 2021 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 { JsonObject } from '@backstage/types';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import { AwsCodeCommitUrlReader, parseUrl } from './AwsCodeCommitUrlReader';
|
||||
import { UrlReaderPredicateTuple } from './types';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { mockClient } from 'aws-sdk-client-mock';
|
||||
import {
|
||||
CodeCommitClient,
|
||||
GetFileCommand,
|
||||
GetFolderCommand,
|
||||
} from '@aws-sdk/client-codecommit';
|
||||
import fs from 'fs';
|
||||
import { DefaultAwsCredentialsManager } from '@backstage/integration-aws-node';
|
||||
import {
|
||||
AwsCodeCommitIntegration,
|
||||
readAwsCodeCommitIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
|
||||
const AMAZON_AWS_CODECOMMIT_HOST = 'console.aws.amazon.com';
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
describe('parseUrl', () => {
|
||||
it('supports all formats (requireGitPath = true)', () => {
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml?region=eu-west-1',
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
path: 'catalog-info.yaml',
|
||||
repositoryName: 'my-repo',
|
||||
region: 'eu-west-1',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo2/browse/--/test1/test2/catalog-info.yaml',
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
path: 'test1/test2/catalog-info.yaml',
|
||||
repositoryName: 'my-repo2',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-repo/browse/refs/heads/develop/--/some-path/catalog-info.yaml',
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
path: 'some-path/catalog-info.yaml',
|
||||
repositoryName: 'my-test-repo',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'refs/heads/develop',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-repo/browse/refs/tags/v1.0/--/some-path/catalog-info.yaml',
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
path: 'some-path/catalog-info.yaml',
|
||||
repositoryName: 'my-test-repo',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'refs/tags/v1.0',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-repo/browse/c1234/--/some-path/catalog-info.yaml',
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
path: 'some-path/catalog-info.yaml',
|
||||
repositoryName: 'my-test-repo',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'c1234',
|
||||
});
|
||||
});
|
||||
it('throw correct error when not providing full url to file (requireGitPath = true)', () => {
|
||||
expect(() =>
|
||||
parseUrl(
|
||||
`https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo`,
|
||||
true,
|
||||
),
|
||||
).toThrow('Please provide full path to yaml file from CodeCommit');
|
||||
});
|
||||
it('supports all formats (requireGitPath = false)', () => {
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/browse?region=eu-west-1',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/browse/refs/heads/develop',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'refs/heads/develop',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/browse/refs/tags/v1.0',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'refs/tags/v1.0',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/browse/c1234',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'c1234',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/browse/c1234/',
|
||||
),
|
||||
).toEqual({
|
||||
path: '/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'c1234',
|
||||
});
|
||||
expect(
|
||||
parseUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs/browse/c1234/--/folder/',
|
||||
),
|
||||
).toEqual({
|
||||
path: 'folder/',
|
||||
repositoryName: 'my-test-techdocs',
|
||||
region: 'eu-west-1',
|
||||
commitSpecifier: 'c1234',
|
||||
});
|
||||
});
|
||||
|
||||
it('throw correct error when providing edit url', () => {
|
||||
expect(() =>
|
||||
parseUrl(
|
||||
`https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/files/edit/refs/heads/develop/--/test/catalog-info.yaml`,
|
||||
),
|
||||
).toThrow(
|
||||
'Please provide the view url to yaml file from CodeCommit, not the edit url',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AwsCodeCommitUrlReader', () => {
|
||||
const codeCommitClient = mockClient(CodeCommitClient);
|
||||
|
||||
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
|
||||
return AwsCodeCommitUrlReader.factory({
|
||||
config: new ConfigReader(config),
|
||||
logger: getVoidLogger(),
|
||||
treeResponseFactory,
|
||||
});
|
||||
};
|
||||
|
||||
it('creates a sample reader without the awsCodeCommit field', () => {
|
||||
const entries = createReader({
|
||||
integrations: {},
|
||||
});
|
||||
|
||||
expect(entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('creates a reader with credentials correctly configured', () => {
|
||||
const awsCodeCommitIntegrations = [];
|
||||
awsCodeCommitIntegrations.push({
|
||||
host: AMAZON_AWS_CODECOMMIT_HOST,
|
||||
accessKeyId: 'fakekey',
|
||||
secretAccessKey: 'fakekey',
|
||||
});
|
||||
|
||||
const entries = createReader({
|
||||
integrations: {
|
||||
awsCodeCommit: awsCodeCommitIntegrations,
|
||||
},
|
||||
});
|
||||
|
||||
expect(entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('creates a reader with default credentials provider', () => {
|
||||
const awsCodeCommitIntegrations = [];
|
||||
awsCodeCommitIntegrations.push({
|
||||
host: AMAZON_AWS_CODECOMMIT_HOST,
|
||||
});
|
||||
|
||||
const entries = createReader({
|
||||
integrations: {
|
||||
awsCodeCommit: awsCodeCommitIntegrations,
|
||||
},
|
||||
});
|
||||
|
||||
expect(entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('predicates', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
awsCodeCommit: [{}],
|
||||
},
|
||||
});
|
||||
const predicate = readers[0].predicate;
|
||||
|
||||
it('returns true for the correct aws CodeCommit storage host', () => {
|
||||
expect(
|
||||
predicate(
|
||||
new URL(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo',
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for a url with the full path and the correct host', () => {
|
||||
expect(
|
||||
predicate(
|
||||
new URL(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml?region=eu-west-1',
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an incorrect host', () => {
|
||||
expect(predicate(new URL('https://amazon.com'))).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a completely different host', () => {
|
||||
expect(predicate(new URL('https://storage.cloud.google.com'))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false for a path that starts with the wrong format', () => {
|
||||
expect(
|
||||
predicate(
|
||||
new URL(
|
||||
'https://s3.console.aws.amazon.com/s3/object/bucket?&bucketType=general&prefix=catalog-info.yaml',
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('read', () => {
|
||||
const [{ reader }] = createReader({
|
||||
integrations: {
|
||||
awsCodeCommit: [
|
||||
{
|
||||
host: 'amazonaws.com',
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
codeCommitClient.reset();
|
||||
|
||||
codeCommitClient.on(GetFileCommand).resolves({
|
||||
fileContent: fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsCodeCommit/awsCodeCommit-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns contents of a file in a repository', async () => {
|
||||
const { buffer } = await reader.readUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml',
|
||||
);
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
const url =
|
||||
'https://eu-west-1.console.aws.NOTamazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml';
|
||||
const expectedSnapshot = `[Error: Could not retrieve file from CodeCommit; caused by Error: Invalid AWS CodeCommit URL (unexpected host format): ${url}]`;
|
||||
await expect(reader.readUrl(url)).rejects.toMatchInlineSnapshot(
|
||||
expectedSnapshot,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readUrl', () => {
|
||||
const [{ reader }] = createReader({
|
||||
integrations: {
|
||||
awsCodeCommit: [
|
||||
{
|
||||
host: AMAZON_AWS_CODECOMMIT_HOST,
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
codeCommitClient.reset();
|
||||
|
||||
codeCommitClient.on(GetFileCommand).resolves({
|
||||
fileContent: fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsCodeCommit/awsCodeCommit-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
commitId: `123abc`,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns contents of a file in a repository via buffer', async () => {
|
||||
const { buffer, etag } = await reader.readUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml',
|
||||
);
|
||||
expect(etag).toBe('123abc');
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
|
||||
it('returns contents of a file in a repository via stream', async () => {
|
||||
const { buffer, etag } = await reader.readUrl(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml',
|
||||
);
|
||||
expect(etag).toBe('123abc');
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
const url =
|
||||
'https://eu-west-1.console.aws.NOTamazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml';
|
||||
const expectedSnapshot = `[Error: Could not retrieve file from CodeCommit; caused by Error: Invalid AWS CodeCommit URL (unexpected host format): ${url}]`;
|
||||
await expect(reader.readUrl!(url)).rejects.toMatchInlineSnapshot(
|
||||
expectedSnapshot,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readUrl with etag', () => {
|
||||
const [{ reader }] = createReader({
|
||||
integrations: {
|
||||
awsCodeCommit: [
|
||||
{
|
||||
host: AMAZON_AWS_CODECOMMIT_HOST,
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
codeCommitClient.reset();
|
||||
|
||||
codeCommitClient.on(GetFileCommand).resolves({
|
||||
fileContent: fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsCodeCommit/awsCodeCommit-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
commitId: `123abc`,
|
||||
blobId: '999',
|
||||
filePath: 'catalog.yaml',
|
||||
fileMode: 'EXECUTABLE',
|
||||
fileSize: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it('Throw NotModifiedError on matching eTag', async () => {
|
||||
await expect(
|
||||
reader.readUrl!(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-repo/browse/--/catalog-info.yaml',
|
||||
{
|
||||
etag: '123abc',
|
||||
},
|
||||
),
|
||||
).rejects.toThrow(NotModifiedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readTree', () => {
|
||||
let awsCodeCommitUrlReader: AwsCodeCommitUrlReader;
|
||||
|
||||
beforeEach(() => {
|
||||
codeCommitClient.reset();
|
||||
|
||||
codeCommitClient
|
||||
.on(GetFolderCommand, {
|
||||
folderPath: `/`,
|
||||
repositoryName: `my-test-techdocs`,
|
||||
commitSpecifier: undefined,
|
||||
})
|
||||
.resolves({
|
||||
files: [
|
||||
{
|
||||
absolutePath: `awsCodeCommit-mock-object.yaml`,
|
||||
relativePath: `awsCodeCommit-mock-object.yaml`,
|
||||
},
|
||||
],
|
||||
subFolders: [
|
||||
{
|
||||
absolutePath: `subFolder`,
|
||||
relativePath: `subFolder`,
|
||||
},
|
||||
],
|
||||
});
|
||||
codeCommitClient
|
||||
.on(GetFolderCommand, {
|
||||
folderPath: `subFolder`,
|
||||
repositoryName: `my-test-techdocs`,
|
||||
commitSpecifier: undefined,
|
||||
})
|
||||
.resolves({
|
||||
files: [
|
||||
{
|
||||
absolutePath: `subFolder/awsCodeCommit-mock-object2.yaml`,
|
||||
relativePath: `subFolder/awsCodeCommit-mock-object2.yaml`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
codeCommitClient
|
||||
.on(GetFileCommand, {
|
||||
filePath: `awsCodeCommit-mock-object.yaml`,
|
||||
commitSpecifier: undefined,
|
||||
repositoryName: `my-test-techdocs`,
|
||||
})
|
||||
.resolves({
|
||||
fileContent: fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsCodeCommit/awsCodeCommit-mock-object.yaml',
|
||||
),
|
||||
),
|
||||
});
|
||||
codeCommitClient
|
||||
.on(GetFileCommand, {
|
||||
filePath: `subFolder/awsCodeCommit-mock-object2.yaml`,
|
||||
commitSpecifier: undefined,
|
||||
repositoryName: `my-test-techdocs`,
|
||||
})
|
||||
.resolves({
|
||||
fileContent: fs.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'__fixtures__/awsCodeCommit/subFolder/awsCodeCommit-mock-object2.yaml',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const config = new ConfigReader({
|
||||
host: AMAZON_AWS_CODECOMMIT_HOST,
|
||||
accessKeyId: 'fake-access-key',
|
||||
secretAccessKey: 'fake-secret-key',
|
||||
});
|
||||
|
||||
const credsManager = DefaultAwsCredentialsManager.fromConfig(config);
|
||||
|
||||
awsCodeCommitUrlReader = new AwsCodeCommitUrlReader(
|
||||
credsManager,
|
||||
new AwsCodeCommitIntegration(
|
||||
readAwsCodeCommitIntegrationConfig(config),
|
||||
),
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
});
|
||||
|
||||
it('returns contents of a file in a repository', async () => {
|
||||
const response = await awsCodeCommitUrlReader.readTree(
|
||||
'https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/my-test-techdocs',
|
||||
);
|
||||
|
||||
const files = await response.files();
|
||||
const bodyRootFile = await files[0].content();
|
||||
const bodySubfolderFile = await files[1].content();
|
||||
|
||||
expect(files.length).toEqual(2);
|
||||
expect(bodyRootFile.toString().trim()).toBe('site_name: Test');
|
||||
expect(bodySubfolderFile.toString().trim()).toBe('site_name: Test2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright 2021 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 {
|
||||
ReaderFactory,
|
||||
ReadTreeOptions,
|
||||
ReadTreeResponse,
|
||||
ReadTreeResponseFactory,
|
||||
ReadUrlOptions,
|
||||
ReadUrlResponse,
|
||||
SearchResponse,
|
||||
UrlReader,
|
||||
} from './types';
|
||||
import {
|
||||
AwsCredentialsManager,
|
||||
DefaultAwsCredentialsManager,
|
||||
} from '@backstage/integration-aws-node';
|
||||
import {
|
||||
AwsCodeCommitIntegration,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { ForwardedError, NotModifiedError } from '@backstage/errors';
|
||||
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
|
||||
import {
|
||||
CodeCommitClient,
|
||||
GetFileCommand,
|
||||
GetFileCommandInput,
|
||||
GetFileCommandOutput,
|
||||
GetFolderCommand,
|
||||
} from '@aws-sdk/client-codecommit';
|
||||
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
|
||||
import { Readable } from 'stream';
|
||||
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
|
||||
import { relative } from 'path/posix';
|
||||
import { AbortController } from '@aws-sdk/abort-controller';
|
||||
|
||||
export function parseUrl(
|
||||
url: string,
|
||||
requireGitPath: boolean = false,
|
||||
): {
|
||||
path: string;
|
||||
repositoryName: string;
|
||||
region: string;
|
||||
commitSpecifier?: string;
|
||||
} {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (parsedUrl.pathname.includes('/files/edit/')) {
|
||||
throw new Error(
|
||||
'Please provide the view url to yaml file from CodeCommit, not the edit url',
|
||||
);
|
||||
}
|
||||
if (requireGitPath && !parsedUrl.pathname.includes('/browse/')) {
|
||||
throw new Error('Please provide full path to yaml file from CodeCommit');
|
||||
}
|
||||
|
||||
const hostMatch = parsedUrl.host.match(
|
||||
/^([^\.]+)\.console\.aws\.amazon\.com$/,
|
||||
);
|
||||
if (!hostMatch) {
|
||||
throw new Error(
|
||||
`Invalid AWS CodeCommit URL (unexpected host format): ${url}`,
|
||||
);
|
||||
}
|
||||
const [, region] = hostMatch;
|
||||
|
||||
const pathMatch = parsedUrl.pathname.match(
|
||||
/^\/codesuite\/codecommit\/repositories\/([^\/]+)\/browse\/((.*)\/)?--\/(.*)$/,
|
||||
);
|
||||
|
||||
if (!pathMatch) {
|
||||
if (!requireGitPath) {
|
||||
const pathname = parsedUrl.pathname
|
||||
.split('/--/')[0]
|
||||
.replace('/codesuite/codecommit/repositories/', '');
|
||||
const [repositoryName, commitSpecifier] = pathname.split('/browse');
|
||||
|
||||
return {
|
||||
region,
|
||||
repositoryName: repositoryName.replace(/^\/|\/$/g, ''),
|
||||
path: '/',
|
||||
commitSpecifier:
|
||||
commitSpecifier === ''
|
||||
? undefined
|
||||
: commitSpecifier?.replace(/^\/|\/$/g, ''),
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid AWS CodeCommit URL (unexpected path format): ${url}`,
|
||||
);
|
||||
}
|
||||
const [, repositoryName, , commitSpecifier, path] = pathMatch;
|
||||
|
||||
return {
|
||||
region,
|
||||
repositoryName,
|
||||
path,
|
||||
// the commitSpecifier is passed to AWS SDK which does not allow empty strings so replace empty string with undefined
|
||||
commitSpecifier: commitSpecifier === '' ? undefined : commitSpecifier,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a {@link @backstage/backend-plugin-api#UrlReaderService} for AWS CodeCommit.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class AwsCodeCommitUrlReader implements UrlReader {
|
||||
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const credsManager = DefaultAwsCredentialsManager.fromConfig(config);
|
||||
|
||||
return integrations.awsCodeCommit.list().map(integration => {
|
||||
const reader = new AwsCodeCommitUrlReader(credsManager, integration, {
|
||||
treeResponseFactory,
|
||||
});
|
||||
const predicate = (url: URL) => {
|
||||
return (
|
||||
url.host.endsWith(integration.config.host) &&
|
||||
url.pathname.startsWith('/codesuite/codecommit')
|
||||
);
|
||||
};
|
||||
|
||||
return { reader, predicate };
|
||||
});
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly credsManager: AwsCredentialsManager,
|
||||
private readonly integration: AwsCodeCommitIntegration,
|
||||
private readonly deps: {
|
||||
treeResponseFactory: ReadTreeResponseFactory;
|
||||
},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* If accessKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:
|
||||
* https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
|
||||
*/
|
||||
private static buildStaticCredentials(
|
||||
accessKeyId: string,
|
||||
secretAccessKey: string,
|
||||
): AwsCredentialIdentityProvider {
|
||||
return async () => {
|
||||
return {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private static async buildCredentials(
|
||||
credsManager: AwsCredentialsManager,
|
||||
region: string,
|
||||
integration?: AwsCodeCommitIntegration,
|
||||
): Promise<AwsCredentialIdentityProvider> {
|
||||
// Fall back to the default credential chain if neither account ID
|
||||
// nor explicit credentials are provided
|
||||
if (!integration) {
|
||||
return (await credsManager.getCredentialProvider()).sdkCredentialProvider;
|
||||
}
|
||||
|
||||
const accessKeyId = integration.config.accessKeyId;
|
||||
const secretAccessKey = integration.config.secretAccessKey;
|
||||
let explicitCredentials: AwsCredentialIdentityProvider;
|
||||
if (accessKeyId && secretAccessKey) {
|
||||
explicitCredentials = AwsCodeCommitUrlReader.buildStaticCredentials(
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
);
|
||||
} else {
|
||||
explicitCredentials = (await credsManager.getCredentialProvider())
|
||||
.sdkCredentialProvider;
|
||||
}
|
||||
|
||||
const roleArn = integration.config.roleArn;
|
||||
if (roleArn) {
|
||||
return fromTemporaryCredentials({
|
||||
masterCredentials: explicitCredentials,
|
||||
params: {
|
||||
RoleSessionName: 'backstage-aws-code-commit-url-reader',
|
||||
RoleArn: roleArn,
|
||||
ExternalId: integration.config.externalId,
|
||||
},
|
||||
clientConfig: { region },
|
||||
});
|
||||
}
|
||||
|
||||
return explicitCredentials;
|
||||
}
|
||||
|
||||
private async buildCodeCommitClient(
|
||||
credsManager: AwsCredentialsManager,
|
||||
region: string,
|
||||
integration: AwsCodeCommitIntegration,
|
||||
): Promise<CodeCommitClient> {
|
||||
const credentials = await AwsCodeCommitUrlReader.buildCredentials(
|
||||
credsManager,
|
||||
region,
|
||||
integration,
|
||||
);
|
||||
|
||||
const codeCommit = new CodeCommitClient({
|
||||
region: region,
|
||||
credentials: credentials,
|
||||
});
|
||||
return codeCommit;
|
||||
}
|
||||
|
||||
async readUrl(
|
||||
url: string,
|
||||
options?: ReadUrlOptions,
|
||||
): Promise<ReadUrlResponse> {
|
||||
// etag and lastModifiedAfter are not supported by the CodeCommit API
|
||||
try {
|
||||
const { path, repositoryName, region, commitSpecifier } = parseUrl(
|
||||
url,
|
||||
true,
|
||||
);
|
||||
const codeCommitClient = await this.buildCodeCommitClient(
|
||||
this.credsManager,
|
||||
region,
|
||||
this.integration,
|
||||
);
|
||||
const abortController = new AbortController();
|
||||
|
||||
const input: GetFileCommandInput = {
|
||||
repositoryName: repositoryName,
|
||||
commitSpecifier: commitSpecifier,
|
||||
filePath: path,
|
||||
};
|
||||
|
||||
options?.signal?.addEventListener('abort', () => abortController.abort());
|
||||
const getObjectCommand = new GetFileCommand(input);
|
||||
const response: GetFileCommandOutput = await codeCommitClient.send(
|
||||
getObjectCommand,
|
||||
{
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
);
|
||||
|
||||
if (options?.etag && options.etag === response.commitId) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
return ReadUrlResponseFactory.fromReadable(
|
||||
Readable.from([response?.fileContent] || []),
|
||||
{
|
||||
etag: response.commitId,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.$metadata && e.$metadata.httpStatusCode === 304) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
if (e.name && e.name === 'NotModifiedError') {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
throw new ForwardedError('Could not retrieve file from CodeCommit', e);
|
||||
}
|
||||
}
|
||||
|
||||
async readTreePath(
|
||||
codeCommitClient: CodeCommitClient,
|
||||
abortSignal: any,
|
||||
path: string,
|
||||
repositoryName: string,
|
||||
commitSpecifier?: string,
|
||||
etag?: string,
|
||||
): Promise<string[]> {
|
||||
const getFolderCommand = new GetFolderCommand({
|
||||
folderPath: path,
|
||||
repositoryName: repositoryName,
|
||||
commitSpecifier: commitSpecifier,
|
||||
});
|
||||
const response = await codeCommitClient.send(getFolderCommand, {
|
||||
abortSignal: abortSignal,
|
||||
});
|
||||
|
||||
if (etag && etag === response.commitId) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
const output: string[] = [];
|
||||
if (response.files) {
|
||||
response.files.forEach(file => {
|
||||
if (file.absolutePath) {
|
||||
output.push(file.absolutePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!response.subFolders) {
|
||||
return output;
|
||||
}
|
||||
|
||||
for (const subFolder of response.subFolders) {
|
||||
if (subFolder.absolutePath) {
|
||||
output.push(
|
||||
...(await this.readTreePath(
|
||||
codeCommitClient,
|
||||
abortSignal,
|
||||
subFolder.absolutePath,
|
||||
repositoryName,
|
||||
commitSpecifier,
|
||||
etag,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async readTree(
|
||||
url: string,
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
// url: https://eu-west-1.console.aws.amazon.com/codesuite/codecommit/repositories/test-stijn-delete-techdocs/browse?region=eu-west-1
|
||||
try {
|
||||
const { path, repositoryName, region, commitSpecifier } = parseUrl(url);
|
||||
const codeCommitClient = await this.buildCodeCommitClient(
|
||||
this.credsManager,
|
||||
region,
|
||||
this.integration,
|
||||
);
|
||||
|
||||
const abortController = new AbortController();
|
||||
options?.signal?.addEventListener('abort', () => abortController.abort());
|
||||
|
||||
const allFiles: string[] = await this.readTreePath(
|
||||
codeCommitClient,
|
||||
abortController.signal,
|
||||
path,
|
||||
repositoryName,
|
||||
commitSpecifier,
|
||||
options?.etag,
|
||||
);
|
||||
const responses = [];
|
||||
|
||||
for (let i = 0; i < allFiles.length; i++) {
|
||||
const getFileCommand = new GetFileCommand({
|
||||
repositoryName: repositoryName,
|
||||
filePath: String(allFiles[i]),
|
||||
commitSpecifier: commitSpecifier,
|
||||
});
|
||||
const response = await codeCommitClient.send(getFileCommand);
|
||||
const objectData = await Readable.from([response?.fileContent] || []);
|
||||
|
||||
responses.push({
|
||||
data: objectData,
|
||||
path: relative(
|
||||
path.startsWith('/') ? path : `/${path}`,
|
||||
allFiles[i].startsWith('/') ? allFiles[i] : `/${allFiles[i]}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return await this.deps.treeResponseFactory.fromReadableArray(responses);
|
||||
} catch (e) {
|
||||
if (e.name && e.name === 'NotModifiedError') {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
throw new ForwardedError(
|
||||
'Could not retrieve file tree from CodeCommit',
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async search(): Promise<SearchResponse> {
|
||||
throw new Error('AwsCodeCommitReader does not implement search');
|
||||
}
|
||||
|
||||
toString() {
|
||||
const secretAccessKey = this.integration.config.secretAccessKey;
|
||||
return `awsCodeCommit{host=${this.integration.config.host},authed=${Boolean(
|
||||
secretAccessKey,
|
||||
)}}`;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { FetchUrlReader } from './FetchUrlReader';
|
||||
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
|
||||
import { AwsS3UrlReader } from './AwsS3UrlReader';
|
||||
import { GiteaUrlReader } from './GiteaUrlReader';
|
||||
import { AwsCodeCommitUrlReader } from './AwsCodeCommitUrlReader';
|
||||
|
||||
/**
|
||||
* Creation options for {@link @backstage/backend-plugin-api#UrlReaderService}.
|
||||
@@ -94,6 +95,7 @@ export class UrlReaders {
|
||||
GitlabUrlReader.factory,
|
||||
GoogleGcsUrlReader.factory,
|
||||
AwsS3UrlReader.factory,
|
||||
AwsCodeCommitUrlReader.factory,
|
||||
FetchUrlReader.factory,
|
||||
]),
|
||||
});
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
site_name: Test
|
||||
+1
@@ -0,0 +1 @@
|
||||
site_name: Test2
|
||||
@@ -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';
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
export * from './awsS3';
|
||||
export * from './awsCodeCommit';
|
||||
export * from './azure';
|
||||
export * from './bitbucket';
|
||||
export * from './bitbucketCloud';
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -363,6 +363,55 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aws-sdk/client-codecommit@npm:^3.350.0":
|
||||
version: 3.540.0
|
||||
resolution: "@aws-sdk/client-codecommit@npm:3.540.0"
|
||||
dependencies:
|
||||
"@aws-crypto/sha256-browser": 3.0.0
|
||||
"@aws-crypto/sha256-js": 3.0.0
|
||||
"@aws-sdk/client-sts": 3.540.0
|
||||
"@aws-sdk/core": 3.535.0
|
||||
"@aws-sdk/credential-provider-node": 3.540.0
|
||||
"@aws-sdk/middleware-host-header": 3.535.0
|
||||
"@aws-sdk/middleware-logger": 3.535.0
|
||||
"@aws-sdk/middleware-recursion-detection": 3.535.0
|
||||
"@aws-sdk/middleware-user-agent": 3.540.0
|
||||
"@aws-sdk/region-config-resolver": 3.535.0
|
||||
"@aws-sdk/types": 3.535.0
|
||||
"@aws-sdk/util-endpoints": 3.540.0
|
||||
"@aws-sdk/util-user-agent-browser": 3.535.0
|
||||
"@aws-sdk/util-user-agent-node": 3.535.0
|
||||
"@smithy/config-resolver": ^2.2.0
|
||||
"@smithy/core": ^1.4.0
|
||||
"@smithy/fetch-http-handler": ^2.5.0
|
||||
"@smithy/hash-node": ^2.2.0
|
||||
"@smithy/invalid-dependency": ^2.2.0
|
||||
"@smithy/middleware-content-length": ^2.2.0
|
||||
"@smithy/middleware-endpoint": ^2.5.0
|
||||
"@smithy/middleware-retry": ^2.2.0
|
||||
"@smithy/middleware-serde": ^2.3.0
|
||||
"@smithy/middleware-stack": ^2.2.0
|
||||
"@smithy/node-config-provider": ^2.3.0
|
||||
"@smithy/node-http-handler": ^2.5.0
|
||||
"@smithy/protocol-http": ^3.3.0
|
||||
"@smithy/smithy-client": ^2.5.0
|
||||
"@smithy/types": ^2.12.0
|
||||
"@smithy/url-parser": ^2.2.0
|
||||
"@smithy/util-base64": ^2.3.0
|
||||
"@smithy/util-body-length-browser": ^2.2.0
|
||||
"@smithy/util-body-length-node": ^2.3.0
|
||||
"@smithy/util-defaults-mode-browser": ^2.2.0
|
||||
"@smithy/util-defaults-mode-node": ^2.3.0
|
||||
"@smithy/util-endpoints": ^1.2.0
|
||||
"@smithy/util-middleware": ^2.2.0
|
||||
"@smithy/util-retry": ^2.2.0
|
||||
"@smithy/util-utf8": ^2.3.0
|
||||
tslib: ^2.6.2
|
||||
uuid: ^9.0.1
|
||||
checksum: 4ccc931eb2ce93fb7640d2006c38cfe59d9d89a8cb04a9e59d3a6a4e62f1bc7e67a737612fe79423c2da45605fa4cd02ec60087a11135fe6210681f7b28138d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aws-sdk/client-cognito-identity@npm:3.540.0":
|
||||
version: 3.540.0
|
||||
resolution: "@aws-sdk/client-cognito-identity@npm:3.540.0"
|
||||
@@ -3271,6 +3320,7 @@ __metadata:
|
||||
resolution: "@backstage/backend-common@workspace:packages/backend-common"
|
||||
dependencies:
|
||||
"@aws-sdk/abort-controller": ^3.347.0
|
||||
"@aws-sdk/client-codecommit": ^3.350.0
|
||||
"@aws-sdk/client-s3": ^3.350.0
|
||||
"@aws-sdk/credential-providers": ^3.350.0
|
||||
"@aws-sdk/types": ^3.347.0
|
||||
|
||||
Reference in New Issue
Block a user