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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './resource-metadata';
2 changes: 1 addition & 1 deletion packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BootstrapSource } from './bootstrap-environment';
import type { StringWithoutPlaceholders } from '../environment';
import type { Tag } from '../tags';
import type { StringWithoutPlaceholders } from '../util/placeholders';

export const BUCKET_NAME_OUTPUT = 'BucketName';
export const REPOSITORY_NAME_OUTPUT = 'ImageRepositoryName';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import type { Export, ListExportsCommandOutput, StackResourceSummary } from '@aws-sdk/client-cloudformation';
import type { SDK } from './aws-auth';
import type { NestedStackTemplates } from './deployments';
import { ToolkitError } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api';
import { resourceMetadata } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata/resource-metadata';
import type { ResourceMetadata } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata/resource-metadata';
import type { SDK } from '../aws-auth';
import type { NestedStackTemplates } from './nested-stack-helpers';
import type { Template } from './stack-helpers';
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
import { resourceMetadata } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata';
import type { ResourceMetadata } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata';

export interface ListStackResources {
listStackResources(): Promise<StackResourceSummary[]>;
Expand Down Expand Up @@ -482,8 +483,6 @@ export class EvaluateCloudFormationTemplate {
}
}

export type Template = { [section: string]: { [headings: string]: any } };

interface ArnParts {
readonly partition: string;
readonly service: string;
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/api/cloudformation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './evaluate-cloudformation-template';
export * from './template-body-parameter';
export * from './nested-stack-helpers';
export * from './stack-helpers';
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as path from 'path';
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
import * as fs from 'fs-extra';
import type { SDK } from '../aws-auth';
import { CloudFormationStack, type Template } from './cloudformation';
import { formatErrorMessage } from '../../util';
import { LazyListStackResources, type ListStackResources } from '../evaluate-cloudformation-template';
import type { SDK } from '../aws-auth';
import { LazyListStackResources, type ListStackResources } from './evaluate-cloudformation-template';
import { CloudFormationStack, type Template } from './stack-helpers';

export interface NestedStackTemplates {
readonly physicalName: string | undefined;
Expand Down
189 changes: 189 additions & 0 deletions packages/aws-cdk/lib/api/cloudformation/stack-helpers.ts
Copy link
Contributor Author

@mrgrain mrgrain Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a collection of helpers that were previously in api/deployments but in reality were used across multiple modules that don't have anything to do with deployments.

Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import type { Stack, Tag } from '@aws-sdk/client-cloudformation';
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
import { formatErrorMessage, deserializeStructure } from '../../util';
import type { ICloudFormationClient } from '../aws-auth';
import { StackStatus } from '../stack-events';

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
*
* 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}'`);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as path from 'path';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as util from 'node:util';
import { type CloudFormationStackArtifact, type Environment, EnvironmentPlaceholders } from '@aws-cdk/cx-api';
import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getEndpointFromInstructions } from '@smithy/middleware-endpoint';
import * as chalk from 'chalk';
import * as fs from 'fs-extra';
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
import { debug, error } from '../../logging';
import { IO, type IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
import { contentHash, toYAML } from '../../util';
import { type AssetManifestBuilder } from '../deployments';
import type { AssetManifestBuilder } from '../deployments';
import type { EnvironmentResources } from '../environment';

export type TemplateBodyParameter = {
Expand All @@ -31,6 +32,7 @@ const LARGE_TEMPLATE_SIZE_KB = 50;
* @param toolkitInfo information about the toolkit stack
*/
export async function makeBodyParameter(
ioHelper: IoHelper,
stack: CloudFormationStackArtifact,
resolvedEnvironment: Environment,
assetManifest: AssetManifestBuilder,
Expand All @@ -53,11 +55,13 @@ export async function makeBodyParameter(

const toolkitInfo = await resources.lookupToolkit();
if (!toolkitInfo.found) {
error(
`The template for stack "${stack.displayName}" is ${Math.round(templateJson.length / 1024)}KiB. ` +
await ioHelper.notify(
IO.DEFAULT_TOOLKIT_ERROR.msg(util.format(
`The template for stack "${stack.displayName}" is ${Math.round(templateJson.length / 1024)}KiB. ` +
`Templates larger than ${LARGE_TEMPLATE_SIZE_KB}KiB must be uploaded to S3.\n` +
'Run the following command in order to setup an S3 bucket in this environment, and then re-deploy:\n\n',
chalk.blue(`\t$ cdk bootstrap ${resolvedEnvironment.name}\n`),
chalk.blue(`\t$ cdk bootstrap ${resolvedEnvironment.name}\n`),
)),
);

throw new ToolkitError('Template too large to deploy ("cdk bootstrap" is required)');
Expand Down Expand Up @@ -86,7 +90,7 @@ export async function makeBodyParameter(
);

const templateURL = `${toolkitInfo.bucketUrl}/${key}`;
debug('Storing template in S3 at:', templateURL);
await ioHelper.notify(IO.DEFAULT_TOOLKIT_DEBUG.msg(`Storing template in S3 at: ${templateURL}`));
return { TemplateURL: templateURL };
}

Expand Down
Loading