UNPKG

@backstage/backend-defaults

Version:

Backend defaults used by Backstage backend apps

290 lines (286 loc) • 9.54 kB
'use strict'; 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