@backstage/backend-defaults
Version:
Backend defaults used by Backstage backend apps
290 lines (286 loc) • 9.54 kB
JavaScript
;
var integrationAwsNode = require('@backstage/integration-aws-node');
var integration = require('@backstage/integration');
var errors = require('@backstage/errors');
var credentialProviders = require('@aws-sdk/credential-providers');
var clientS3 = require('@aws-sdk/client-s3');
var abortController = require('@aws-sdk/abort-controller');
var ReadUrlResponseFactory = require('./ReadUrlResponseFactory.cjs.js');
var stream = require('stream');
var posix = require('path/posix');
const DEFAULT_REGION = "us-east-1";
function parseUrl(url, config) {
const parsedUrl = new URL(url);
const pathname = parsedUrl.pathname.substring(1);
const host = parsedUrl.host;
if (config.host === "amazonaws.com" || config.host === "amazonaws.com.cn") {
const match = host.match(
/^(?:([a-z0-9.-]+)\.)?s3(?:[.-]([a-z0-9-]+))?\.amazonaws\.com(\.cn)?$/
);
if (!match) {
throw new Error(`Invalid AWS S3 URL ${url}`);
}
const [, hostBucket, hostRegion] = match;
if (config.s3ForcePathStyle || !hostBucket) {
const slashIndex = pathname.indexOf("/");
if (slashIndex < 0) {
throw new Error(
`Invalid path-style AWS S3 URL ${url}, does not contain bucket in the path`
);
}
return {
path: pathname.substring(slashIndex + 1),
bucket: pathname.substring(0, slashIndex),
region: hostRegion ?? DEFAULT_REGION
};
}
return {
path: pathname,
bucket: hostBucket,
region: hostRegion ?? DEFAULT_REGION
};
}
const usePathStyle = config.s3ForcePathStyle || host.length === config.host.length;
if (usePathStyle) {
const slashIndex = pathname.indexOf("/");
if (slashIndex < 0) {
throw new Error(
`Invalid path-style AWS S3 URL ${url}, does not contain bucket in the path`
);
}
return {
path: pathname.substring(slashIndex + 1),
bucket: pathname.substring(0, slashIndex),
region: DEFAULT_REGION
};
}
return {
path: pathname,
bucket: host.substring(0, host.length - config.host.length - 1),
region: DEFAULT_REGION
};
}
class AwsS3UrlReader {
static factory = ({ config, treeResponseFactory }) => {
const integrations = integration.ScmIntegrations.fromConfig(config);
const credsManager = integrationAwsNode.DefaultAwsCredentialsManager.fromConfig(config);
return integrations.awsS3.list().map((integration) => {
const reader = new AwsS3UrlReader(credsManager, integration, {
treeResponseFactory
});
const predicate = (url) => url.host.endsWith(integration.config.host);
return { reader, predicate };
});
};
credsManager;
integration;
deps;
constructor(credsManager, integration, deps) {
this.credsManager = credsManager;
this.integration = integration;
this.deps = deps;
}
/**
* 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
*/
static buildStaticCredentials(accessKeyId, secretAccessKey) {
return async () => {
return {
accessKeyId,
secretAccessKey
};
};
}
static async buildCredentials(credsManager, region, integration) {
if (!integration) {
return (await credsManager.getCredentialProvider()).sdkCredentialProvider;
}
const accessKeyId = integration.config.accessKeyId;
const secretAccessKey = integration.config.secretAccessKey;
let explicitCredentials;
if (accessKeyId && secretAccessKey) {
explicitCredentials = AwsS3UrlReader.buildStaticCredentials(
accessKeyId,
secretAccessKey
);
} else {
explicitCredentials = (await credsManager.getCredentialProvider()).sdkCredentialProvider;
}
const roleArn = integration.config.roleArn;
if (roleArn) {
return credentialProviders.fromTemporaryCredentials({
masterCredentials: explicitCredentials,
params: {
RoleSessionName: "backstage-aws-s3-url-reader",
RoleArn: roleArn,
ExternalId: integration.config.externalId
},
clientConfig: { region }
});
}
return explicitCredentials;
}
async buildS3Client(credsManager, region, integration) {
const credentials = await AwsS3UrlReader.buildCredentials(
credsManager,
region,
integration
);
const s3 = new clientS3.S3Client({
customUserAgent: "backstage-aws-s3-url-reader",
region,
credentials,
endpoint: integration.config.endpoint,
forcePathStyle: integration.config.s3ForcePathStyle
});
return s3;
}
async retrieveS3ObjectData(stream$1) {
return new Promise((resolve, reject) => {
try {
const chunks = [];
stream$1.on("data", (chunk) => chunks.push(chunk));
stream$1.on(
"error",
(e) => reject(new errors.ForwardedError("Unable to read stream", e))
);
stream$1.on("end", () => resolve(stream.Readable.from(Buffer.concat(chunks))));
} catch (e) {
throw new errors.ForwardedError("Unable to parse the response data", e);
}
});
}
async read(url) {
const response = await this.readUrl(url);
return response.buffer();
}
async readUrl(url, options) {
const { etag, lastModifiedAfter } = options ?? {};
try {
const { path, bucket, region } = parseUrl(url, this.integration.config);
const s3Client = await this.buildS3Client(
this.credsManager,
region,
this.integration
);
const abortController$1 = new abortController.AbortController();
const params = {
Bucket: bucket,
Key: path,
...etag && { IfNoneMatch: etag },
...lastModifiedAfter && {
IfModifiedSince: lastModifiedAfter
}
};
options?.signal?.addEventListener("abort", () => abortController$1.abort());
const getObjectCommand = new clientS3.GetObjectCommand(params);
const response = await s3Client.send(getObjectCommand, {
abortSignal: abortController$1.signal
});
const s3ObjectData = await this.retrieveS3ObjectData(
response.Body
);
return ReadUrlResponseFactory.ReadUrlResponseFactory.fromReadable(s3ObjectData, {
etag: response.ETag,
lastModifiedAt: response.LastModified
});
} catch (e) {
if (e.$metadata && e.$metadata.httpStatusCode === 304) {
throw new errors.NotModifiedError();
}
throw new errors.ForwardedError("Could not retrieve file from S3", e);
}
}
async readTree(url, options) {
try {
const { path, bucket, region } = parseUrl(url, this.integration.config);
const s3Client = await this.buildS3Client(
this.credsManager,
region,
this.integration
);
const abortController$1 = new abortController.AbortController();
const allObjects = [];
const responses = [];
let continuationToken;
let output;
do {
const listObjectsV2Command = new clientS3.ListObjectsV2Command({
Bucket: bucket,
ContinuationToken: continuationToken,
Prefix: path
});
options?.signal?.addEventListener(
"abort",
() => abortController$1.abort()
);
output = await s3Client.send(listObjectsV2Command, {
abortSignal: abortController$1.signal
});
if (output.Contents) {
output.Contents.forEach((contents) => {
allObjects.push(contents.Key);
});
}
continuationToken = output.NextContinuationToken;
} while (continuationToken);
for (let i = 0; i < allObjects.length; i++) {
const getObjectCommand = new clientS3.GetObjectCommand({
Bucket: bucket,
Key: String(allObjects[i])
});
const response = await s3Client.send(getObjectCommand);
const s3ObjectData = await this.retrieveS3ObjectData(
response.Body
);
responses.push({
data: s3ObjectData,
path: posix.relative(path, String(allObjects[i])),
lastModifiedAt: response?.LastModified ?? void 0
});
}
return await this.deps.treeResponseFactory.fromReadableArray(responses);
} catch (e) {
throw new errors.ForwardedError("Could not retrieve file tree from S3", e);
}
}
async search(url, options) {
const { path } = parseUrl(url, this.integration.config);
if (path.match(/[*?]/)) {
throw new Error("Unsupported search pattern URL");
}
try {
const data = await this.readUrl(url, options);
return {
files: [
{
url,
content: data.buffer,
lastModifiedAt: data.lastModifiedAt
}
],
etag: data.etag ?? ""
};
} catch (error) {
errors.assertError(error);
if (error.name === "NotFoundError") {
return {
files: [],
etag: ""
};
}
throw error;
}
}
toString() {
const secretAccessKey = this.integration.config.secretAccessKey;
return `awsS3{host=${this.integration.config.host},authed=${Boolean(
secretAccessKey
)}}`;
}
}
exports.AwsS3UrlReader = AwsS3UrlReader;
exports.DEFAULT_REGION = DEFAULT_REGION;
exports.parseUrl = parseUrl;
//# sourceMappingURL=AwsS3UrlReader.cjs.js.map