packages/@aws-cdk/toolkit-lib/lib/api/toolkit-info.ts (160 lines of code) (raw):
import { format } from 'util';
import type * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import type { SDK } from './aws-auth/private';
import {
BOOTSTRAP_VARIANT_PARAMETER,
BOOTSTRAP_VERSION_OUTPUT,
BUCKET_DOMAIN_NAME_OUTPUT,
BUCKET_NAME_OUTPUT,
DEFAULT_BOOTSTRAP_VARIANT,
REPOSITORY_NAME_OUTPUT,
} from './bootstrap/bootstrap-props';
import type { CloudFormationStack } from './cloudformation';
import { stabilizeStack } from './deployments/cfn-api';
import { IO, type IoHelper } from './io/private';
import { ToolkitError } from './toolkit-error';
export const DEFAULT_TOOLKIT_STACK_NAME = 'CDKToolkit';
/**
* Information on the Bootstrap stack of the environment we're deploying to.
*
* This class serves to:
*
* - Inspect the bootstrap stack, and return various properties of it for successful
* asset deployment (in case of legacy-synthesized stacks).
* - Validate the version of the target environment, and nothing else (in case of
* default-synthesized stacks).
*
* An object of this type might represent a bootstrap stack that could not be found.
* This is not an issue unless any members are used that require the bootstrap stack
* to have been found, in which case an error is thrown (default-synthesized stacks
* should never run into this as they don't need information from the bootstrap
* stack, all information is already encoded into the Cloud Assembly Manifest).
*
* Nevertheless, an instance of this class exists to serve as a cache for SSM
* parameter lookups (otherwise, the "bootstrap stack version" parameter would
* need to be read repeatedly).
*
* Called "ToolkitInfo" for historical reasons.
*
*/
export abstract class ToolkitInfo {
public static determineName(overrideName?: string) {
return overrideName ?? DEFAULT_TOOLKIT_STACK_NAME;
}
public static async lookup(
environment: cxapi.Environment,
sdk: SDK,
ioHelper: IoHelper,
stackName: string | undefined,
): Promise<ToolkitInfo> {
const cfn = sdk.cloudFormation();
stackName = ToolkitInfo.determineName(stackName);
try {
const stack = await stabilizeStack(cfn, ioHelper, stackName);
if (!stack) {
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(
format(
"The environment %s doesn't have the CDK toolkit stack (%s) installed. Use %s to setup your environment for use with the toolkit.",
environment.name,
stackName,
chalk.blue(`cdk bootstrap "${environment.name}"`),
),
));
return ToolkitInfo.bootstrapStackNotFoundInfo(stackName);
}
if (stack.stackStatus.isCreationFailure) {
// Treat a "failed to create" bootstrap stack as an absent one.
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(
format(
'The environment %s has a CDK toolkit stack (%s) that failed to create. Use %s to try provisioning it again.',
environment.name,
stackName,
chalk.blue(`cdk bootstrap "${environment.name}"`),
),
));
return ToolkitInfo.bootstrapStackNotFoundInfo(stackName);
}
return new ExistingToolkitInfo(stack);
} catch (e: any) {
return ToolkitInfo.bootstrapStackLookupError(stackName, e);
}
}
public static fromStack(stack: CloudFormationStack): ToolkitInfo {
return new ExistingToolkitInfo(stack);
}
public static bootstrapStackNotFoundInfo(stackName: string): ToolkitInfo {
return new BootstrapStackNotFoundInfo(
stackName,
"This deployment requires a bootstrap stack with a known name; pass '--toolkit-stack-name' or switch to using the 'DefaultStackSynthesizer' (see https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)",
);
}
public static bootstrapStackLookupError(stackName: string, e: Error): ToolkitInfo {
return new BootstrapStackNotFoundInfo(
stackName,
`This deployment requires a bootstrap stack with a known name, but during its lookup the following error occurred: ${e}; pass \'--toolkit-stack-name\' or switch to using the \'DefaultStackSynthesizer\' (see https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html)`,
);
}
public abstract readonly found: boolean;
public abstract readonly bucketUrl: string;
public abstract readonly bucketName: string;
public abstract readonly repositoryName: string;
public abstract readonly version: number;
public abstract readonly variant: string;
public abstract readonly bootstrapStack: CloudFormationStack;
public abstract readonly stackName: string;
constructor() {
}
}
/**
* Returned when a bootstrap stack is found
*/
class ExistingToolkitInfo extends ToolkitInfo {
public readonly found = true;
constructor(public readonly bootstrapStack: CloudFormationStack) {
super();
}
public get bucketUrl() {
return `https://${this.requireOutput(BUCKET_DOMAIN_NAME_OUTPUT)}`;
}
public get bucketName() {
return this.requireOutput(BUCKET_NAME_OUTPUT);
}
public get repositoryName() {
return this.requireOutput(REPOSITORY_NAME_OUTPUT);
}
public get version() {
return parseInt(this.bootstrapStack.outputs[BOOTSTRAP_VERSION_OUTPUT] ?? '0', 10);
}
public get variant() {
return this.bootstrapStack.parameters[BOOTSTRAP_VARIANT_PARAMETER] ?? DEFAULT_BOOTSTRAP_VARIANT;
}
public get parameters(): Record<string, string> {
return this.bootstrapStack.parameters ?? {};
}
public get terminationProtection(): boolean {
return this.bootstrapStack.terminationProtection ?? false;
}
public get stackName(): string {
return this.bootstrapStack.stackName;
}
/**
* Prepare an ECR repository for uploading to using Docker
*
*/
private requireOutput(output: string): string {
if (!(output in this.bootstrapStack.outputs)) {
throw new ToolkitError(
`The CDK toolkit stack (${this.bootstrapStack.stackName}) does not have an output named ${output}. Use 'cdk bootstrap' to correct this.`,
);
}
return this.bootstrapStack.outputs[output];
}
}
/**
* Returned when a bootstrap stack could not be found
*
* This is not an error in principle, UNTIL one of the members is called that requires
* the bootstrap stack to have been found, in which case the lookup error is still thrown
* belatedly.
*
* The errors below serve as a last stop-gap message--normally calling code should have
* checked `toolkit.found` and produced an appropriate error message.
*/
class BootstrapStackNotFoundInfo extends ToolkitInfo {
public readonly found = false;
constructor(
public readonly stackName: string,
private readonly errorMessage: string,
) {
super();
}
public get bootstrapStack(): CloudFormationStack {
throw new ToolkitError(this.errorMessage);
}
public get bucketUrl(): string {
throw new ToolkitError(this.errorMessage);
}
public get bucketName(): string {
throw new ToolkitError(this.errorMessage);
}
public get repositoryName(): string {
throw new ToolkitError(this.errorMessage);
}
public get version(): number {
throw new ToolkitError(this.errorMessage);
}
public get variant(): string {
throw new ToolkitError(this.errorMessage);
}
public prepareEcrRepository(): Promise<EcrRepositoryInfo> {
throw new ToolkitError(this.errorMessage);
}
}
export interface EcrRepositoryInfo {
repositoryUri: string;
}
export interface EcrCredentials {
username: string;
password: string;
endpoint: string;
}