integ/scripts/node/stack-order.ts (134 lines of code) (raw):
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* This is a command-line tool that parses a synthesized CDK manifest file and outputs the correct stack deployment or
* destroy order based on the stack dependencies.
*
* Stack names are output one per line for ease of use in shell scripts.
*/
/* eslint-disable no-console */
import * as fs from 'fs';
import * as path from 'path';
/**
* The default path to look for the CDK cloud assembly manifest.
*/
const DEFAULT_MANIFEST_PATH = 'cdk.out/manifest.json';
/**
* Represents which type of stack ordering is desired.
*/
enum OrderType {
/**
* Output the stacks in their correct order for a CDK deployment.
*
* Stacks without dependency stacks are output first.
*/
DEPLOY,
/**
* Output the stacks in their correct order for a CDK destroy.
*
* Stacks without dependency stacks are output last.
*/
DESTROY,
}
/**
* Parsed program arguments as specified from the command-line.
*/
interface ProgramArguments {
/**
* The path to a cdk.out/manifest.json file of a synthesized CDK application
*/
readonly manifestPath: string;
/**
* Whether the user desires the stack deployment order or stack destroy order
*/
readonly orderType: OrderType;
}
/**
* A partial definition of an artifact definition within a CDK manifest file
*/
interface Artifact {
/**
* The type of artifact
*/
readonly type: string;
/**
* The key name of other artifacts that this artifact depends on
*/
readonly dependencies?: string[];
}
/**
* A partial JSON schema of a manifest.json file synthesized by CDK.
*/
interface Manifest {
/**
* The artifacts listed in the manifest
*/
readonly artifacts: Record<string, Artifact>;
}
/**
* A minimal internal representation of a CDK stack
*/
interface Stack {
/**
* The stack name
*/
readonly name: string;
/**
* The names of the stacks that this stack depends on.
*/
readonly dependencies: string[];
}
/**
* Returns the command-line usage of this tool suitable for console output.
*/
function usage() {
const baseName = path.basename(process.argv[1]);
return `Usage:
${baseName} [-r] [MANIFEST_PATH]
Arguments:
MANIFEST_PATH
The path to CDK's synthesized manifest.json file. By default, CDK writes
this file to a directory named "cdk.out" in the root of the CDK app.
If not specified this defaults to "${DEFAULT_MANIFEST_PATH}".
-r
Reverses the order. Use this to output the stack destroy order. If not
specified, the default is to output stack deploy order.`;
}
/**
* Processes the command-line arguments and returns a parsed representation.
*
* Throws an `Error` with a user-facing error message if arguments are invalid.
*/
function parseProgramArguments(): ProgramArguments {
let orderType: OrderType = OrderType.DEPLOY;
let manifestPath: string = DEFAULT_MANIFEST_PATH;
let reverseFlag: string | undefined;
// Strip the first two arguments (node interpreter and the path to this script)
const args = process.argv.slice(2);
if (args.length === 2) {
// Two arguments passed. Ensure the first is the "-r" flag
[ reverseFlag, manifestPath ] = args;
// Validate first arg is the -r flag
if (reverseFlag !== '-r') {
throw new Error(`Unexpected argument: "${reverseFlag}"`);
}
orderType = OrderType.DESTROY;
} else if (args.length === 1) {
if (args[0] === '-r') {
orderType = OrderType.DESTROY;
} else {
// A single argument is passed containing the manifest path
manifestPath = process.argv[2];
}
} else if (args.length > 2) {
throw new Error(`Unexpected number of arguments (${args.length})`);
}
return {
manifestPath,
orderType,
};
}
/**
* An asynchronous function to read a UTF-8 encoded file.
*
* @param filePath The path of the file to be read
*/
async function readFileAsync(filePath: string) {
return new Promise<string>((resolve, reject) => {
fs.readFile(filePath, { encoding: 'utf-8' }, (err, data) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
}
/**
* Scans a parsed CDK manifest JSON structure and returns the stacks contained.
*
* @param manifest A parsed CDK manifest
*/
function findStacks(manifest: Manifest): Stack[] {
// Stacks are top-level nodes in the "artifcats" object.
return Object.entries<Artifact>(manifest.artifacts)
.filter(entry => entry[1].type == 'aws:cloudformation:stack')
.map(entry => {
const [ name, artifact ] = entry;
return {
name,
dependencies: artifact.dependencies ?? [],
};
});
}
/**
* Orders stacks in their proper deploy/destroy order.
*
* @param stacks The stacks to be sorted
* @param orderType The type of ordering to apply
*/
function sortStacks(stacks: Stack[], orderType: OrderType): Stack[] {
/**
* A set data structure of remaining stack names to be picked.
*/
const remainingStacks: Set<string> = new Set<string>(stacks.map(s => s.name));
/**
* The sorted result array that we will accumulate stacks into
*/
let sortedStacks: Stack[] = [];
function hasPendingDependencies(stack: Stack): boolean {
return stack.dependencies?.some(depStack => remainingStacks.has(depStack));
}
// Stacks with no dependencies remaining are picked on each loop iteration of the loop until there are no remaining stacks.
while(remainingStacks.size > 0) {
// Consider each remaining stack
remainingStacks.forEach(stackName => {
// Find the stack object by its name
const stack = stacks.find(val => val.name == stackName)!;
// We can deploy this stack if it has no remaining (or un-picked) dependencies
if (!hasPendingDependencies(stack)) {
sortedStacks.push(stack);
remainingStacks.delete(stackName);
}
});
}
// For destroy order, we reverse the list
if (orderType == OrderType.DESTROY) {
sortedStacks = sortedStacks.reverse();
}
return sortedStacks;
}
/**
* The entrypoint of the program.
*
* This processes and validates the command line arguments. It exits and
* displays an error/usage output if the arguments are invalid.
*
* If the arguments are valid, it reads the specified CDK manifest file, sorts
* the stacks in their correct deploy/destroy order, and outputs their names
* in the resulting order - one per line.
*/
async function main() {
let args: ProgramArguments;
try {
args = parseProgramArguments();
} catch(e: any) {
console.error(e.toString());
console.error(usage());
process.exit(1);
}
const manifestRaw = await readFileAsync(args.manifestPath);
let manifest: Manifest | undefined;
// Parse the JSON and cast to a Manifest
try {
manifest = JSON.parse(manifestRaw) as Manifest;
} catch (e: any) {
throw new Error(`${args.manifestPath} is not a valid JSON file`);
}
const stacks = findStacks(manifest);
const sortedStacks = sortStacks(stacks, args.orderType);
const sortedStackNames = sortedStacks.map(stack => stack.name);
for (let stackName of sortedStackNames) {
console.log(stackName);
}
}
main()
.catch(e => {
if (e instanceof Error) {
console.error(e.toString());
if (e.stack) {
console.error(e.stack.toString());
}
process.exit(1);
}
});