packages/@aws-cdk/toolkit-lib/lib/api/cloudformation/stack-helpers.ts (109 lines of code) (raw):
import type { Stack, Tag } from '@aws-sdk/client-cloudformation';
import { formatErrorMessage, deserializeStructure } from '../../util';
import type { ICloudFormationClient } from '../aws-auth/private';
import { StackStatus } from '../stack-events';
import { ToolkitError } from '../toolkit-error';
export interface Template {
Parameters?: Record<string, TemplateParameter>;
[section: string]: any;
}
export interface TemplateParameter {
Type: string;
Default?: any;
Description?: string;
[key: string]: any;
}
/**
* Represents an (existing) Stack in CloudFormation
*
* Bundle and cache some information that we need during deployment (so we don't have to make
* repeated calls to CloudFormation).
*/
export class CloudFormationStack {
public static async lookup(
cfn: ICloudFormationClient,
stackName: string,
retrieveProcessedTemplate: boolean = false,
): Promise<CloudFormationStack> {
try {
const response = await cfn.describeStacks({ StackName: stackName });
return new CloudFormationStack(cfn, stackName, response.Stacks && response.Stacks[0], retrieveProcessedTemplate);
} catch (e: any) {
if (e.name === 'ValidationError' && formatErrorMessage(e) === `Stack with id ${stackName} does not exist`) {
return new CloudFormationStack(cfn, stackName, undefined);
}
throw e;
}
}
/**
* Return a copy of the given stack that does not exist
*
* It's a little silly that it needs arguments to do that, but there we go.
*/
public static doesNotExist(cfn: ICloudFormationClient, stackName: string) {
return new CloudFormationStack(cfn, stackName);
}
/**
* From static information (for testing)
*/
public static fromStaticInformation(cfn: ICloudFormationClient, stackName: string, stack: Stack) {
return new CloudFormationStack(cfn, stackName, stack);
}
private _template: any;
protected constructor(
private readonly cfn: ICloudFormationClient,
public readonly stackName: string,
private readonly stack?: Stack,
private readonly retrieveProcessedTemplate: boolean = false,
) {
}
/**
* Retrieve the stack's deployed template
*
* Cached, so will only be retrieved once. Will return an empty
* structure if the stack does not exist.
*/
public async template(): Promise<Template> {
if (!this.exists) {
return {};
}
if (this._template === undefined) {
const response = await this.cfn.getTemplate({
StackName: this.stackName,
TemplateStage: this.retrieveProcessedTemplate ? 'Processed' : 'Original',
});
this._template = (response.TemplateBody && deserializeStructure(response.TemplateBody)) || {};
}
return this._template;
}
/**
* Whether the stack exists
*/
public get exists() {
return this.stack !== undefined;
}
/**
* The stack's ID (which is the same as its ARN)
*
* Throws if the stack doesn't exist.
*/
public get stackId() {
this.assertExists();
return this.stack!.StackId!;
}
/**
* The stack's current outputs
*
* Empty object if the stack doesn't exist
*/
public get outputs(): Record<string, string> {
if (!this.exists) {
return {};
}
const result: { [name: string]: string } = {};
(this.stack!.Outputs || []).forEach((output) => {
result[output.OutputKey!] = output.OutputValue!;
});
return result;
}
/**
* The stack's status
*
* Special status NOT_FOUND if the stack does not exist.
*/
public get stackStatus(): StackStatus {
if (!this.exists) {
return new StackStatus('NOT_FOUND', 'Stack not found during lookup');
}
return StackStatus.fromStackDescription(this.stack!);
}
/**
* The stack's current tags
*
* Empty list if the stack does not exist
*/
public get tags(): Tag[] {
return this.stack?.Tags || [];
}
/**
* SNS Topic ARNs that will receive stack events.
*
* Empty list if the stack does not exist
*/
public get notificationArns(): string[] {
return this.stack?.NotificationARNs ?? [];
}
/**
* Return the names of all current parameters to the stack
*
* Empty list if the stack does not exist.
*/
public get parameterNames(): string[] {
return Object.keys(this.parameters);
}
/**
* Return the names and values of all current parameters to the stack
*
* Empty object if the stack does not exist.
*/
public get parameters(): Record<string, string> {
if (!this.exists) {
return {};
}
const ret: Record<string, string> = {};
for (const param of this.stack!.Parameters ?? []) {
ret[param.ParameterKey!] = param.ResolvedValue ?? param.ParameterValue!;
}
return ret;
}
/**
* Return the termination protection of the stack
*/
public get terminationProtection(): boolean | undefined {
return this.stack?.EnableTerminationProtection;
}
private assertExists() {
if (!this.exists) {
throw new ToolkitError(`No stack named '${this.stackName}'`);
}
}
}