Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion packages/@aws-cdk/toolkit/lib/api/cloud-assembly/stack-assembly.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as cxapi from '@aws-cdk/cx-api';
import { CloudAssembly } from 'aws-cdk/lib/api/cxapp/cloud-assembly';
import { CloudAssembly, sanitizePatterns, StackCollection, ExtendedStackSelection as CliExtendedStackSelection } from 'aws-cdk/lib/api/cxapp/cloud-assembly';
import { major } from 'semver';
import { ICloudAssemblySource } from './types';
import { ExtendedStackSelection, StackSelectionStrategy, StackSelector } from '../../types';
import { ToolkitError } from '../errors';

/**
* A single Cloud Assembly wrapped to provide additional stack operations.
Expand All @@ -9,4 +12,99 @@ export class StackAssembly extends CloudAssembly implements ICloudAssemblySource
public async produce(): Promise<cxapi.CloudAssembly> {
return this.assembly;
}

/**
* Improved stack selection interface with a single selector
* @returns
* @throws when the assembly does not contain any stacks, unless `selector.failOnEmpty` is `false`
* @throws when individual selection strategies are not satisfied
*/
public selectStacksV2(selector: StackSelector): StackCollection {
const asm = this.assembly;
const topLevelStacks = asm.stacks;
const allStacks = major(asm.version) < 10 ? asm.stacks : asm.stacksRecursively;

if (allStacks.length === 0 && (selector.failOnEmpty ?? true)) {
throw new ToolkitError('This app contains no stacks');
}

const extend = convertExtend(selector.extend);
const patterns = sanitizePatterns(selector.patterns ?? []);

switch (selector.strategy) {
case StackSelectionStrategy.ALL_STACKS:
return new StackCollection(this, allStacks);
case StackSelectionStrategy.MAIN_ASSEMBLY:
if (topLevelStacks.length < 1) {
//@todo text should probably be handled in io host
throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest');
}
return this.extendStacks(topLevelStacks, allStacks, extend);
case StackSelectionStrategy.ONLY_SINGLE:
if (topLevelStacks.length !== 1) {
//@todo text should probably be handled in io host
throw new ToolkitError('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' +
`Stacks: ${allStacks.map(x => x.hierarchicalId).join(' · ')}`);
}
return new StackCollection(this, topLevelStacks);
default:
const matched = this.selectMatchingStacks(allStacks, patterns, extend);
if (
selector.strategy === StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE
&& matched.stackCount !== 1
) {
//@todo text should probably be handled in io host
throw new ToolkitError(
`Stack selection is ambiguous, please choose a specific stack for import [${allStacks.map(x => x.hierarchicalId).join(',')}]`,
);
}
if (
selector.strategy === StackSelectionStrategy.PATTERN_MUST_MATCH
&& matched.stackCount < 1
) {
//@todo text should probably be handled in io host
throw new ToolkitError(
`Stack selection is ambiguous, please choose a specific stack for import [${allStacks.map(x => x.hierarchicalId).join(',')}]`,
);
}

return matched;
}
}

/**
* Select all stacks.
*
* This method never throws and can safely be used as a basis for other calculations.
*
* @returns a `StackCollection` of all stacks
*/
public selectAllStacks() {
const allStacks = major(this.assembly.version) < 10 ? this.assembly.stacks : this.assembly.stacksRecursively;
return new StackCollection(this, allStacks);
}

/**
* Select all stacks that have the validateOnSynth flag et.
*
* @param assembly
* @returns a `StackCollection` of all stacks that needs to be validated
*/
public selectStacksForValidation() {
const allStacks = this.selectAllStacks();
return allStacks.filter((art) => art.validateOnSynth ?? false);
}
}

function convertExtend(extend?: ExtendedStackSelection): CliExtendedStackSelection | undefined {
switch (extend) {
case ExtendedStackSelection.DOWNSTREAM:
return CliExtendedStackSelection.Downstream;
case ExtendedStackSelection.UPSTREAM:
return CliExtendedStackSelection.Upstream;
case ExtendedStackSelection.NONE:
return CliExtendedStackSelection.None;
default:
return undefined;
}
}
34 changes: 9 additions & 25 deletions packages/@aws-cdk/toolkit/lib/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ export class Toolkit {
public async synth(cx: ICloudAssemblySource, options: SynthOptions): Promise<ICloudAssemblySource> {
const ioHost = withAction(this.ioHost, 'synth');
const assembly = await this.assemblyFromSource(cx);
const stacks = await assembly.selectStacks(options.stacks, false);
const autoValidateStacks = options.validateStacks ? await this.selectStacksForValidation(assembly) : new StackCollection(assembly, []);
this.processStackMessages(stacks.concat(autoValidateStacks));
const stacks = assembly.selectStacksV2(options.stacks);
const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : [];
this.processStackMessages(stacks.concat(...autoValidateStacks));

// if we have a single stack, print it to STDOUT
if (stacks.stackCount === 1) {
Expand Down Expand Up @@ -123,7 +123,7 @@ export class Toolkit {
public async diff(cx: ICloudAssemblySource, options: DiffOptions): Promise<boolean> {
const ioHost = withAction(this.ioHost, 'diff');
const assembly = await this.assemblyFromSource(cx);
const stacks = await assembly.selectStacks(options.stacks, {});
const stacks = await assembly.selectStacksV2(options.stacks);
throw new Error('Not implemented yet');
}

Expand All @@ -134,7 +134,7 @@ export class Toolkit {
const ioHost = withAction(this.ioHost, 'deploy');
const timer = Timer.start();
const assembly = await this.assemblyFromSource(cx);
const stackCollection = await assembly.selectStacks(options.stacks, {});
const stackCollection = assembly.selectStacksV2(options.stacks);

const synthTime = timer.end();
await ioHost.notify(info(`\n✨ Synthesis time: ${synthTime.asSec}s\n`, 'CDK_TOOLKIT_I5001', {
Expand Down Expand Up @@ -219,11 +219,8 @@ export class Toolkit {
} else {
await ioHost.notify(warning(`${chalk.bold(stack.displayName)}: stack has no resources, deleting existing stack.`));
await this._destroy(assembly, 'deploy', {
selector: { patterns: [stack.hierarchicalId] },
exclusively: true,
force: true,
stacks: { patterns: [stack.hierarchicalId], strategy: StackSelectionStrategy.PATTERN_MUST_MATCH_SINGLE },
roleArn: options.roleArn,
fromDeploy: true,
ci: options.ci,
});
}
Expand Down Expand Up @@ -321,7 +318,7 @@ export class Toolkit {
}

// Perform a rollback
await this.rollback({
await this.rollback(cx, {
selector: { patterns: [stack.hierarchicalId] },
toolkitStackName: options.toolkitStackName,
force: options.force,
Expand Down Expand Up @@ -454,7 +451,7 @@ export class Toolkit {
*
* Rolls back the selected stacks.
*/
public async rollback(_cx: ICloudAssemblySource, _options: WatchOptions): Promise<void> {
public async rollback(_cx: ICloudAssemblySource, _options: any): Promise<void> {
const ioHost = withAction(this.ioHost, 'rollback');
throw new Error('Not implemented yet');
}
Expand All @@ -474,7 +471,7 @@ export class Toolkit {
*/
private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise<void> {
const ioHost = withAction(this.ioHost, action);
let stacks = await assembly.selectStacks(options.stacks, false);
let stacks = await assembly.selectStacksV2(options.stacks);

// The stacks will have been ordered for deployment, so reverse them for deletion.
stacks = stacks.reversed();
Expand Down Expand Up @@ -523,8 +520,6 @@ export class Toolkit {

/**
* Create a deployments class
* @param action C
* @returns
*/
private async deploymentsForAction(action: ToolkitAction): Promise<Deployments> {
return new Deployments({
Expand All @@ -533,17 +528,6 @@ export class Toolkit {
});
}

/**
* Select all stacks that have the validateOnSynth flag et.
*
* @param assembly
* @returns a `StackCollection` of all stacks that needs to be validated
*/
private async selectStacksForValidation(assembly: StackAssembly) {
const allStacks = await assembly.selectStacks({ strategy: StackSelectionStrategy.ALL_STACKS }, false);
return allStacks.filter((art) => art.validateOnSynth ?? false);
}

/**
* Validate the stacks for errors and warnings according to the CLI's current settings
* @deprecated remove and use directly in synth
Expand Down
63 changes: 45 additions & 18 deletions packages/@aws-cdk/toolkit/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,41 @@ export type ToolkitAction =

export enum StackSelectionStrategy {
/**
* Returns an empty selection in case there are no stacks.
* Returns all stacks in the app regardless of patterns,
* including stacks inside nested assemblies.
*/
NONE = 'none',
ALL_STACKS = 'ALL_STACKS',

/**
* Return matched stacks. If no patterns are provided, return the single stack in the app.
* If the app has more than one stack, an error is thrown.
*
* This is the default strategy used by "deploy" and "destroy".
* Returns all stacks in the main (top level) assembly only.
*/
MATCH_OR_SINGLE = 'match-or-single',
MAIN_ASSEMBLY = 'MAIN_ASSEMBLY',

/**
* Throws an exception if the selector doesn't match at least one stack in the app.
* If the assembly includes a single stack, returns it.
* Otherwise throws an exception.
*/
MUST_MATCH_PATTERN = 'must-match-pattern',
ONLY_SINGLE = 'ONLY_SINGLE',

/**
* Returns all stacks in the main (top level) assembly only.
* @todo not currently publicly exposed
* Return stacks matched by patterns.
* If no stacks are found, execution is halted successfully.
* Most likely you don't want to use this but `StackSelectionStrategy.MUST_MATCH_PATTERN`
*/
MAIN_ASSEMBLY = 'main',
PATTERN_MATCH = 'PATTERN_MATCH',

/**
* If no selectors are provided, returns all stacks in the app,
* including stacks inside nested assemblies.
* Return stacks matched by patterns.
* Throws an exception if the patterns don't match at least one stack in the assembly.
*/
ALL_STACKS = 'all',
PATTERN_MUST_MATCH = 'PATTERN_MUST_MATCH',

/**
* Returns if exactly one stack is matched by the pattern(s).
* Throws an exception if no stack, or more than exactly one stack are matched.
*/
PATTERN_MUST_MATCH_SINGLE = 'PATTERN_MUST_MATCH_SINGLE',
}

/**
Expand All @@ -60,25 +68,44 @@ export enum ExtendedStackSelection {
* Include stacks that depend on this stack
*/
DOWNSTREAM = 'downstream',

/**
* @TODO
* Include both directions.
* I.e. stacks that this stack depends on, and stacks that depend on this stack.
*/
// FULL = 'full',
}

/**
* A specification of which stacks should be selected
*/
export interface StackSelector {
/**
* The behavior if if no selectors are provided.
*/
strategy: StackSelectionStrategy;

/**
* A list of patterns to match the stack hierarchical ids
* Only used with `PATTERN_*` selection strategies.
*/
patterns?: string[];

/**
* Extend the selection to upstream/downstream stacks
* @default ExtendedStackSelection.None only select the specified stacks.
* Extend the selection to upstream/downstream stacks.
* @default ExtendedStackSelection.None only select the specified/matched stacks
*/
extend?: ExtendedStackSelection;

/**
* The behavior if if no selectors are provided.
* By default, we throw an exception if the assembly contains no stacks.
* Set to `false`, to halt execution for empty assemblies without error.
*
* Note that actions can still throw if a stack selection result is empty,
* but the assembly contains stacks in principle.
*
* @default true
*/
strategy: StackSelectionStrategy;
failOnEmpty?: boolean;
}
10 changes: 5 additions & 5 deletions packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class CloudAssembly {
}
}

private selectMatchingStacks(
protected selectMatchingStacks(
stacks: cxapi.CloudFormationStackArtifact[],
patterns: string[],
extend: ExtendedStackSelection = ExtendedStackSelection.None,
Expand Down Expand Up @@ -170,7 +170,7 @@ export class CloudAssembly {
}
}

private extendStacks(
protected extendStacks(
matched: cxapi.CloudFormationStackArtifact[],
all: cxapi.CloudFormationStackArtifact[],
extend: ExtendedStackSelection = ExtendedStackSelection.None,
Expand Down Expand Up @@ -241,8 +241,8 @@ export class StackCollection {
return new StackCollection(this.assembly, this.stackArtifacts.filter(predicate));
}

public concat(other: StackCollection): StackCollection {
return new StackCollection(this.assembly, this.stackArtifacts.concat(other.stackArtifacts));
public concat(...others: StackCollection[]): StackCollection {
return new StackCollection(this.assembly, this.stackArtifacts.concat(...others.map(o => o.stackArtifacts)));
}

/**
Expand Down Expand Up @@ -380,7 +380,7 @@ function includeUpstreamStacks(
}
}

function sanitizePatterns(patterns: string[]): string[] {
export function sanitizePatterns(patterns: string[]): string[] {
let sanitized = patterns.filter(s => s != null); // filter null/undefined
sanitized = [...new Set(sanitized)]; // make them unique
return sanitized;
Expand Down
Loading