374 lines
50 KiB
JavaScript
374 lines
50 KiB
JavaScript
"use strict";
|
|||
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
|
exports.CloudAssemblyBuilder = exports.CloudAssembly = void 0;
|
||
|
|
const fs = require("fs");
|
||
|
|
const os = require("os");
|
||
|
|
const path = require("path");
|
||
|
|
const cxschema = require("@aws-cdk/cloud-assembly-schema/lib");
|
||
|
|
const cloudformation_artifact_1 = require("./artifacts/cloudformation-artifact");
|
||
|
|
const nested_cloud_assembly_artifact_1 = require("./artifacts/nested-cloud-assembly-artifact");
|
||
|
|
const tree_cloud_artifact_1 = require("./artifacts/tree-cloud-artifact");
|
||
|
|
const cloud_artifact_1 = require("./cloud-artifact");
|
||
|
|
const error_1 = require("./private/error");
|
||
|
|
const toposort_1 = require("./private/toposort");
|
||
|
|
const CLOUD_ASSEMBLY_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssembly');
|
||
|
|
/**
|
||
|
|
* The name of the root manifest file of the assembly.
|
||
|
|
*/
|
||
|
|
const MANIFEST_FILE = 'manifest.json';
|
||
|
|
/**
|
||
|
|
* Represents a deployable cloud application.
|
||
|
|
*/
|
||
|
|
class CloudAssembly {
|
||
|
|
/**
|
||
|
|
* Return whether the given object is a CloudAssembly.
|
||
|
|
*
|
||
|
|
* We do attribute detection since we can't reliably use 'instanceof'.
|
||
|
|
*/
|
||
|
|
static isCloudAssembly(x) {
|
||
|
|
return x !== null && typeof (x) === 'object' && CLOUD_ASSEMBLY_SYMBOL in x;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Cleans up any temporary assembly directories that got created in this process
|
||
|
|
*
|
||
|
|
* If a Cloud Assembly is emitted to a temporary directory, its directory gets
|
||
|
|
* added to a list. This function iterates over that list and deletes each
|
||
|
|
* directory in it, to free up disk space.
|
||
|
|
*
|
||
|
|
* This function will normally be called automatically during Node process
|
||
|
|
* exit and so you don't need to call this. However, some test environments do
|
||
|
|
* not properly trigger Node's `exit` event. Notably: Jest does not trigger
|
||
|
|
* the `exit` event (<https://github.com/jestjs/jest/issues/10927>).
|
||
|
|
*
|
||
|
|
* ## Cleaning up temporary directories in jest
|
||
|
|
*
|
||
|
|
* For Jest, you have to make sure this function is called at the end of the
|
||
|
|
* test suite instead:
|
||
|
|
*
|
||
|
|
* ```js
|
||
|
|
* import { CloudAssembly } from 'aws-cdk-lib/cx-api';
|
||
|
|
*
|
||
|
|
* afterAll(CloudAssembly.cleanupTemporaryDirectories);
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* Alternatively, you can use the `setupFilesAfterEnv` feature and use a
|
||
|
|
* provided helper script to automatically inject the above into every
|
||
|
|
* test file, so you don't have to do it by hand.
|
||
|
|
*
|
||
|
|
* ```
|
||
|
|
* $ npx jest --setupFilesAfterEnv aws-cdk-lib/testhelpers/jest-autoclean
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* Or put the following into `jest.config.js`:
|
||
|
|
*
|
||
|
|
* ```js
|
||
|
|
* module.exports = {
|
||
|
|
* // ...
|
||
|
|
* setupFilesAfterEnv: ['aws-cdk-lib/testhelpers/jest-cleanup'],
|
||
|
|
* };
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
static cleanupTemporaryDirectories() {
|
||
|
|
for (const dir of TEMPORARY_ASSEMBLY_DIRS) {
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
TEMPORARY_ASSEMBLY_DIRS.splice(0, TEMPORARY_ASSEMBLY_DIRS.length);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Reads a cloud assembly from the specified directory.
|
||
|
|
* @param directory - The root directory of the assembly.
|
||
|
|
*/
|
||
|
|
constructor(directory, loadOptions) {
|
||
|
|
this.directory = directory;
|
||
|
|
this.manifest = cxschema.Manifest.loadAssemblyManifest(path.join(directory, MANIFEST_FILE), loadOptions);
|
||
|
|
this.version = this.manifest.version;
|
||
|
|
this.artifacts = this.renderArtifacts(loadOptions?.topoSort ?? true);
|
||
|
|
this.runtime = this.manifest.runtime || { libraries: {} };
|
||
|
|
Object.defineProperty(this, CLOUD_ASSEMBLY_SYMBOL, { value: true });
|
||
|
|
// force validation of deps by accessing 'depends' on all artifacts
|
||
|
|
this.validateDeps();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Attempts to find an artifact with a specific identity.
|
||
|
|
* @returns A `CloudArtifact` object or `undefined` if the artifact does not exist in this assembly.
|
||
|
|
* @param id - The artifact ID
|
||
|
|
*/
|
||
|
|
tryGetArtifact(id) {
|
||
|
|
return this.artifacts.find(a => a.id === id);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns a CloudFormation stack artifact from this assembly.
|
||
|
|
*
|
||
|
|
* Will only search the current assembly.
|
||
|
|
*
|
||
|
|
* @param stackName - the name of the CloudFormation stack.
|
||
|
|
* @throws if there is no stack artifact by that name
|
||
|
|
* @throws if there is more than one stack with the same stack name. You can
|
||
|
|
* use `getStackArtifact(stack.artifactId)` instead.
|
||
|
|
* @returns a `CloudFormationStackArtifact` object.
|
||
|
|
*/
|
||
|
|
getStackByName(stackName) {
|
||
|
|
const artifacts = this.artifacts.filter(a => a instanceof cloudformation_artifact_1.CloudFormationStackArtifact && a.stackName === stackName);
|
||
|
|
if (!artifacts || artifacts.length === 0) {
|
||
|
|
throw new error_1.CloudAssemblyError(`Unable to find stack with stack name "${stackName}"`);
|
||
|
|
}
|
||
|
|
if (artifacts.length > 1) {
|
||
|
|
throw new error_1.CloudAssemblyError(`There are multiple stacks with the stack name "${stackName}" (${artifacts.map(a => a.id).join(',')}). Use "getStackArtifact(id)" instead`);
|
||
|
|
}
|
||
|
|
return artifacts[0];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns a CloudFormation stack artifact by name from this assembly.
|
||
|
|
* @deprecated renamed to `getStackByName` (or `getStackArtifact(id)`)
|
||
|
|
*/
|
||
|
|
getStack(stackName) {
|
||
|
|
return this.getStackByName(stackName);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns a CloudFormation stack artifact from this assembly.
|
||
|
|
*
|
||
|
|
* @param artifactId - the artifact id of the stack (can be obtained through `stack.artifactId`).
|
||
|
|
* @throws if there is no stack artifact with that id
|
||
|
|
* @returns a `CloudFormationStackArtifact` object.
|
||
|
|
*/
|
||
|
|
getStackArtifact(artifactId) {
|
||
|
|
const artifact = this.tryGetArtifactRecursively(artifactId);
|
||
|
|
if (!artifact) {
|
||
|
|
throw new error_1.CloudAssemblyError(`Unable to find artifact with id "${artifactId}"`);
|
||
|
|
}
|
||
|
|
if (!(artifact instanceof cloudformation_artifact_1.CloudFormationStackArtifact)) {
|
||
|
|
throw new error_1.CloudAssemblyError(`Artifact ${artifactId} is not a CloudFormation stack`);
|
||
|
|
}
|
||
|
|
return artifact;
|
||
|
|
}
|
||
|
|
tryGetArtifactRecursively(artifactId) {
|
||
|
|
return this.stacksRecursively.find(a => a.id === artifactId);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns all the stacks, including the ones in nested assemblies
|
||
|
|
*/
|
||
|
|
get stacksRecursively() {
|
||
|
|
function search(stackArtifacts, assemblies) {
|
||
|
|
if (assemblies.length === 0) {
|
||
|
|
return stackArtifacts;
|
||
|
|
}
|
||
|
|
const [head, ...tail] = assemblies;
|
||
|
|
const nestedAssemblies = head.nestedAssemblies.map(asm => asm.nestedAssembly);
|
||
|
|
return search(stackArtifacts.concat(head.stacks), tail.concat(nestedAssemblies));
|
||
|
|
}
|
||
|
|
return search([], [this]);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns a nested assembly artifact.
|
||
|
|
*
|
||
|
|
* @param artifactId - The artifact ID of the nested assembly
|
||
|
|
*/
|
||
|
|
getNestedAssemblyArtifact(artifactId) {
|
||
|
|
const artifact = this.tryGetArtifact(artifactId);
|
||
|
|
if (!artifact) {
|
||
|
|
throw new error_1.CloudAssemblyError(`Unable to find artifact with id "${artifactId}"`);
|
||
|
|
}
|
||
|
|
if (!(artifact instanceof nested_cloud_assembly_artifact_1.NestedCloudAssemblyArtifact)) {
|
||
|
|
throw new error_1.CloudAssemblyError(`Found artifact '${artifactId}' but it's not a nested cloud assembly`);
|
||
|
|
}
|
||
|
|
return artifact;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns a nested assembly.
|
||
|
|
*
|
||
|
|
* @param artifactId - The artifact ID of the nested assembly
|
||
|
|
*/
|
||
|
|
getNestedAssembly(artifactId) {
|
||
|
|
return this.getNestedAssemblyArtifact(artifactId).nestedAssembly;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Returns the tree metadata artifact from this assembly.
|
||
|
|
* @throws if there is no metadata artifact by that name
|
||
|
|
* @returns a `TreeCloudArtifact` object if there is one defined in the manifest, `undefined` otherwise.
|
||
|
|
*/
|
||
|
|
tree() {
|
||
|
|
const trees = this.artifacts.filter(a => a.manifest.type === cxschema.ArtifactType.CDK_TREE);
|
||
|
|
if (trees.length === 0) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
else if (trees.length > 1) {
|
||
|
|
throw new error_1.CloudAssemblyError(`Multiple artifacts of type ${cxschema.ArtifactType.CDK_TREE} found in manifest`);
|
||
|
|
}
|
||
|
|
const tree = trees[0];
|
||
|
|
if (!(tree instanceof tree_cloud_artifact_1.TreeCloudArtifact)) {
|
||
|
|
throw new error_1.CloudAssemblyError('"Tree" artifact is not of expected type');
|
||
|
|
}
|
||
|
|
return tree;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* @returns all the CloudFormation stack artifacts that are included in this assembly.
|
||
|
|
*/
|
||
|
|
get stacks() {
|
||
|
|
return this.artifacts.filter(isCloudFormationStackArtifact);
|
||
|
|
function isCloudFormationStackArtifact(x) {
|
||
|
|
return x instanceof cloudformation_artifact_1.CloudFormationStackArtifact;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* The nested assembly artifacts in this assembly
|
||
|
|
*/
|
||
|
|
get nestedAssemblies() {
|
||
|
|
return this.artifacts.filter(isNestedCloudAssemblyArtifact);
|
||
|
|
function isNestedCloudAssemblyArtifact(x) {
|
||
|
|
return x instanceof nested_cloud_assembly_artifact_1.NestedCloudAssemblyArtifact;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
validateDeps() {
|
||
|
|
for (const artifact of this.artifacts) {
|
||
|
|
ignore(artifact.dependencies);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
renderArtifacts(topoSort) {
|
||
|
|
const result = new Array();
|
||
|
|
for (const [name, artifact] of Object.entries(this.manifest.artifacts || {})) {
|
||
|
|
const cloudartifact = cloud_artifact_1.CloudArtifact.fromManifest(this, name, artifact);
|
||
|
|
if (cloudartifact) {
|
||
|
|
result.push(cloudartifact);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return topoSort ? (0, toposort_1.topologicalSort)(result, x => x.id, x => x._dependencyIDs) : result;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
exports.CloudAssembly = CloudAssembly;
|
||
|
|
/**
|
||
|
|
* Can be used to build a cloud assembly.
|
||
|
|
*/
|
||
|
|
class CloudAssemblyBuilder {
|
||
|
|
/**
|
||
|
|
* Initializes a cloud assembly builder.
|
||
|
|
* @param outdir - The output directory, uses temporary directory if undefined
|
||
|
|
*/
|
||
|
|
constructor(outdir, props = {}) {
|
||
|
|
this.artifacts = {};
|
||
|
|
this.missing = new Array();
|
||
|
|
this.outdir = determineOutputDirectory(outdir);
|
||
|
|
this.assetOutdir = props.assetOutdir ?? this.outdir;
|
||
|
|
this.parentBuilder = props.parentBuilder;
|
||
|
|
// we leverage the fact that outdir is long-lived to avoid staging assets into it
|
||
|
|
// that were already staged (copying can be expensive). this is achieved by the fact
|
||
|
|
// that assets use a source hash as their name. other artifacts, and the manifest itself,
|
||
|
|
// will overwrite existing files as needed.
|
||
|
|
ensureDirSync(this.outdir);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Adds an artifact into the cloud assembly.
|
||
|
|
* @param id - The ID of the artifact.
|
||
|
|
* @param manifest - The artifact manifest
|
||
|
|
*/
|
||
|
|
addArtifact(id, manifest) {
|
||
|
|
this.artifacts[id] = filterUndefined(manifest);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Reports that some context is missing in order for this cloud assembly to be fully synthesized.
|
||
|
|
* @param missing - Missing context information.
|
||
|
|
*/
|
||
|
|
addMissing(missing) {
|
||
|
|
if (this.missing.every(m => m.key !== missing.key)) {
|
||
|
|
this.missing.push(missing);
|
||
|
|
}
|
||
|
|
// Also report in parent
|
||
|
|
this.parentBuilder?.addMissing(missing);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Finalizes the cloud assembly into the output directory returns a
|
||
|
|
* `CloudAssembly` object that can be used to inspect the assembly.
|
||
|
|
*/
|
||
|
|
buildAssembly(options = {}) {
|
||
|
|
// explicitly initializing this type will help us detect
|
||
|
|
// breaking changes. (For example adding a required property will break compilation).
|
||
|
|
let manifest = {
|
||
|
|
version: cxschema.Manifest.version(),
|
||
|
|
artifacts: this.artifacts,
|
||
|
|
runtime: options.runtimeInfo,
|
||
|
|
missing: this.missing.length > 0 ? this.missing : undefined,
|
||
|
|
};
|
||
|
|
// now we can filter
|
||
|
|
manifest = filterUndefined(manifest);
|
||
|
|
const manifestFilePath = path.join(this.outdir, MANIFEST_FILE);
|
||
|
|
cxschema.Manifest.saveAssemblyManifest(manifest, manifestFilePath);
|
||
|
|
// "backwards compatibility": in order for the old CLI to tell the user they
|
||
|
|
// need a new version, we'll emit the legacy manifest with only "version".
|
||
|
|
// this will result in an error "CDK Toolkit >= CLOUD_ASSEMBLY_VERSION is required in order to interact with this program."
|
||
|
|
fs.writeFileSync(path.join(this.outdir, 'cdk.out'), JSON.stringify({ version: manifest.version }));
|
||
|
|
return new CloudAssembly(this.outdir);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Creates a nested cloud assembly
|
||
|
|
*/
|
||
|
|
createNestedAssembly(artifactId, displayName) {
|
||
|
|
const directoryName = artifactId;
|
||
|
|
const innerAsmDir = path.join(this.outdir, directoryName);
|
||
|
|
this.addArtifact(artifactId, {
|
||
|
|
type: cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY,
|
||
|
|
properties: {
|
||
|
|
directoryName,
|
||
|
|
displayName,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
return new CloudAssemblyBuilder(innerAsmDir, {
|
||
|
|
// Reuse the same asset output directory as the current Casm builder
|
||
|
|
assetOutdir: this.assetOutdir,
|
||
|
|
parentBuilder: this,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Delete the cloud assembly directory
|
||
|
|
*/
|
||
|
|
delete() {
|
||
|
|
fs.rmSync(this.outdir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
exports.CloudAssemblyBuilder = CloudAssemblyBuilder;
|
||
|
|
/**
|
||
|
|
* Returns a copy of `obj` without undefined values in maps or arrays.
|
||
|
|
*/
|
||
|
|
function filterUndefined(obj) {
|
||
|
|
if (Array.isArray(obj)) {
|
||
|
|
return obj.filter(x => x !== undefined).map(x => filterUndefined(x));
|
||
|
|
}
|
||
|
|
if (typeof (obj) === 'object') {
|
||
|
|
const ret = {};
|
||
|
|
for (const [key, value] of Object.entries(obj)) {
|
||
|
|
if (value === undefined) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
ret[key] = filterUndefined(value);
|
||
|
|
}
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
return obj;
|
||
|
|
}
|
||
|
|
function ignore(_x) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Turn the given optional output directory into a fixed output directory
|
||
|
|
*/
|
||
|
|
function determineOutputDirectory(outdir) {
|
||
|
|
if (outdir) {
|
||
|
|
return outdir;
|
||
|
|
}
|
||
|
|
// Make a temporary directory; clean it up automatically if this is done for testing.
|
||
|
|
const tmpDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out'));
|
||
|
|
TEMPORARY_ASSEMBLY_DIRS.push(tmpDir);
|
||
|
|
return outdir ?? tmpDir;
|
||
|
|
}
|
||
|
|
function ensureDirSync(dir) {
|
||
|
|
if (fs.existsSync(dir)) {
|
||
|
|
if (!fs.statSync(dir).isDirectory()) {
|
||
|
|
throw new error_1.CloudAssemblyError(`${dir} must be a directory`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
fs.mkdirSync(dir, { recursive: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// On process exit, delete all temporary assembly directories
|
||
|
|
const TEMPORARY_ASSEMBLY_DIRS = [];
|
||
|
|
process.on('exit', () => CloudAssembly.cleanupTemporaryDirectories());
|
||
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xvdWQtYXNzZW1ibHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjbG91ZC1hc3NlbWJseS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSx5QkFBeUI7QUFDekIseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUU3QiwrREFBK0Q7QUFDL0QsaUZBQWtGO0FBQ2xGLCtGQUF5RjtBQUN6Rix5RUFBb0U7QUFDcEUscURBQWlEO0FBQ2pELDJDQUFxRDtBQUNyRCxpREFBcUQ7QUFFckQsTUFBTSxxQkFBcUIsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLCtCQUErQixDQUFDLENBQUM7QUFFMUU7O0dBRUc7QUFDSCxNQUFNLGFBQWEsR0FBRyxlQUFlLENBQUM7QUFFdEM7O0dBRUc7QUFDSCxNQUFhLGFBQWE7SUFDeEI7Ozs7T0FJRztJQUNJLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBTTtRQUNsQyxPQUFPLENBQUMsS0FBSyxJQUFJLElBQUksT0FBTSxDQUFDLENBQUMsQ0FBQyxLQUFLLFFBQVEsSUFBSSxxQkFBcUIsSUFBSSxDQUFDLENBQUM7SUFDNUUsQ0FBQztJQUVEOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7T0F1Q0c7SUFDSSxNQUFNLENBQUMsMkJBQTJCO1FBQ3ZDLEtBQUssTUFBTSxHQUFHLElBQUksdUJBQXVCLEVBQUUsQ0FBQztZQUMxQyxFQUFFLENBQUMsTUFBTSxDQUFDLEdBQUcsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7UUFDbkQsQ0FBQztRQUNELHVCQUF1QixDQUFDLE1BQU0sQ0FBQyxDQUFDLEVBQUUsdUJBQXVCLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDcEUsQ0FBQztJQTJCRDs7O09BR0c7SUFDSCxZQUFZLFNBQWlCLEVBQUUsV0FBMEM7UUFDdkUsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUM7UUFFM0IsSUFBSSxDQUFDLFFB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
|