Skip to content

Commit ee0d87d

Browse files
authored
refactor(cli): new internal cloudformation api module (#270)
Pulls some shared "cloudformation" helpers into a new internal api module. This resolves some annoying inter-dependencies between internal modules and specifically breaks up the `deployment` mega module a bit. Also moves some other files from `api/util` so that it's now almost gone (which is the goal). --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 0d7bcca commit ee0d87d

40 files changed

+300
-281
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './resource-metadata';

packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { BootstrapSource } from './bootstrap-environment';
2+
import type { StringWithoutPlaceholders } from '../environment';
23
import type { Tag } from '../tags';
3-
import type { StringWithoutPlaceholders } from '../util/placeholders';
44

55
export const BUCKET_NAME_OUTPUT = 'BucketName';
66
export const REPOSITORY_NAME_OUTPUT = 'ImageRepositoryName';

packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts renamed to packages/aws-cdk/lib/api/cloudformation/evaluate-cloudformation-template.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
22
import type { Export, ListExportsCommandOutput, StackResourceSummary } from '@aws-sdk/client-cloudformation';
3-
import type { SDK } from './aws-auth';
4-
import type { NestedStackTemplates } from './deployments';
5-
import { ToolkitError } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api';
6-
import { resourceMetadata } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata/resource-metadata';
7-
import type { ResourceMetadata } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata/resource-metadata';
3+
import type { SDK } from '../aws-auth';
4+
import type { NestedStackTemplates } from './nested-stack-helpers';
5+
import type { Template } from './stack-helpers';
6+
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
7+
import { resourceMetadata } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata';
8+
import type { ResourceMetadata } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/resource-metadata';
89

910
export interface ListStackResources {
1011
listStackResources(): Promise<StackResourceSummary[]>;
@@ -482,8 +483,6 @@ export class EvaluateCloudFormationTemplate {
482483
}
483484
}
484485

485-
export type Template = { [section: string]: { [headings: string]: any } };
486-
487486
interface ArnParts {
488487
readonly partition: string;
489488
readonly service: string;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './evaluate-cloudformation-template';
2+
export * from './template-body-parameter';
3+
export * from './nested-stack-helpers';
4+
export * from './stack-helpers';

packages/aws-cdk/lib/api/deployments/nested-stack-helpers.ts renamed to packages/aws-cdk/lib/api/cloudformation/nested-stack-helpers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as path from 'path';
22
import type { CloudFormationStackArtifact } from '@aws-cdk/cx-api';
33
import * as fs from 'fs-extra';
4-
import type { SDK } from '../aws-auth';
5-
import { CloudFormationStack, type Template } from './cloudformation';
64
import { formatErrorMessage } from '../../util';
7-
import { LazyListStackResources, type ListStackResources } from '../evaluate-cloudformation-template';
5+
import type { SDK } from '../aws-auth';
6+
import { LazyListStackResources, type ListStackResources } from './evaluate-cloudformation-template';
7+
import { CloudFormationStack, type Template } from './stack-helpers';
88

99
export interface NestedStackTemplates {
1010
readonly physicalName: string | undefined;
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import type { Stack, Tag } from '@aws-sdk/client-cloudformation';
2+
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
3+
import { formatErrorMessage, deserializeStructure } from '../../util';
4+
import type { ICloudFormationClient } from '../aws-auth';
5+
import { StackStatus } from '../stack-events';
6+
7+
export interface Template {
8+
Parameters?: Record<string, TemplateParameter>;
9+
[section: string]: any;
10+
}
11+
12+
export interface TemplateParameter {
13+
Type: string;
14+
Default?: any;
15+
Description?: string;
16+
[key: string]: any;
17+
}
18+
19+
/**
20+
* Represents an (existing) Stack in CloudFormation
21+
*
22+
* Bundle and cache some information that we need during deployment (so we don't have to make
23+
* repeated calls to CloudFormation).
24+
*/
25+
export class CloudFormationStack {
26+
public static async lookup(
27+
cfn: ICloudFormationClient,
28+
stackName: string,
29+
retrieveProcessedTemplate: boolean = false,
30+
): Promise<CloudFormationStack> {
31+
try {
32+
const response = await cfn.describeStacks({ StackName: stackName });
33+
return new CloudFormationStack(cfn, stackName, response.Stacks && response.Stacks[0], retrieveProcessedTemplate);
34+
} catch (e: any) {
35+
if (e.name === 'ValidationError' && formatErrorMessage(e) === `Stack with id ${stackName} does not exist`) {
36+
return new CloudFormationStack(cfn, stackName, undefined);
37+
}
38+
throw e;
39+
}
40+
}
41+
42+
/**
43+
* Return a copy of the given stack that does not exist
44+
*
45+
* It's a little silly that it needs arguments to do that, but there we go.
46+
*/
47+
public static doesNotExist(cfn: ICloudFormationClient, stackName: string) {
48+
return new CloudFormationStack(cfn, stackName);
49+
}
50+
51+
/**
52+
* From static information (for testing)
53+
*/
54+
public static fromStaticInformation(cfn: ICloudFormationClient, stackName: string, stack: Stack) {
55+
return new CloudFormationStack(cfn, stackName, stack);
56+
}
57+
58+
private _template: any;
59+
60+
protected constructor(
61+
private readonly cfn: ICloudFormationClient,
62+
public readonly stackName: string,
63+
private readonly stack?: Stack,
64+
private readonly retrieveProcessedTemplate: boolean = false,
65+
) {
66+
}
67+
68+
/**
69+
* Retrieve the stack's deployed template
70+
*
71+
* Cached, so will only be retrieved once. Will return an empty
72+
* structure if the stack does not exist.
73+
*/
74+
public async template(): Promise<Template> {
75+
if (!this.exists) {
76+
return {};
77+
}
78+
79+
if (this._template === undefined) {
80+
const response = await this.cfn.getTemplate({
81+
StackName: this.stackName,
82+
TemplateStage: this.retrieveProcessedTemplate ? 'Processed' : 'Original',
83+
});
84+
this._template = (response.TemplateBody && deserializeStructure(response.TemplateBody)) || {};
85+
}
86+
return this._template;
87+
}
88+
89+
/**
90+
* Whether the stack exists
91+
*/
92+
public get exists() {
93+
return this.stack !== undefined;
94+
}
95+
96+
/**
97+
* The stack's ID
98+
*
99+
* Throws if the stack doesn't exist.
100+
*/
101+
public get stackId() {
102+
this.assertExists();
103+
return this.stack!.StackId!;
104+
}
105+
106+
/**
107+
* The stack's current outputs
108+
*
109+
* Empty object if the stack doesn't exist
110+
*/
111+
public get outputs(): Record<string, string> {
112+
if (!this.exists) {
113+
return {};
114+
}
115+
const result: { [name: string]: string } = {};
116+
(this.stack!.Outputs || []).forEach((output) => {
117+
result[output.OutputKey!] = output.OutputValue!;
118+
});
119+
return result;
120+
}
121+
122+
/**
123+
* The stack's status
124+
*
125+
* Special status NOT_FOUND if the stack does not exist.
126+
*/
127+
public get stackStatus(): StackStatus {
128+
if (!this.exists) {
129+
return new StackStatus('NOT_FOUND', 'Stack not found during lookup');
130+
}
131+
return StackStatus.fromStackDescription(this.stack!);
132+
}
133+
134+
/**
135+
* The stack's current tags
136+
*
137+
* Empty list if the stack does not exist
138+
*/
139+
public get tags(): Tag[] {
140+
return this.stack?.Tags || [];
141+
}
142+
143+
/**
144+
* SNS Topic ARNs that will receive stack events.
145+
*
146+
* Empty list if the stack does not exist
147+
*/
148+
public get notificationArns(): string[] {
149+
return this.stack?.NotificationARNs ?? [];
150+
}
151+
152+
/**
153+
* Return the names of all current parameters to the stack
154+
*
155+
* Empty list if the stack does not exist.
156+
*/
157+
public get parameterNames(): string[] {
158+
return Object.keys(this.parameters);
159+
}
160+
161+
/**
162+
* Return the names and values of all current parameters to the stack
163+
*
164+
* Empty object if the stack does not exist.
165+
*/
166+
public get parameters(): Record<string, string> {
167+
if (!this.exists) {
168+
return {};
169+
}
170+
const ret: Record<string, string> = {};
171+
for (const param of this.stack!.Parameters ?? []) {
172+
ret[param.ParameterKey!] = param.ResolvedValue ?? param.ParameterValue!;
173+
}
174+
return ret;
175+
}
176+
177+
/**
178+
* Return the termination protection of the stack
179+
*/
180+
public get terminationProtection(): boolean | undefined {
181+
return this.stack?.EnableTerminationProtection;
182+
}
183+
184+
private assertExists() {
185+
if (!this.exists) {
186+
throw new ToolkitError(`No stack named '${this.stackName}'`);
187+
}
188+
}
189+
}

packages/aws-cdk/lib/api/util/template-body-parameter.ts renamed to packages/aws-cdk/lib/api/cloudformation/template-body-parameter.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import * as path from 'path';
1+
import * as fs from 'node:fs/promises';
2+
import * as path from 'node:path';
3+
import * as util from 'node:util';
24
import { type CloudFormationStackArtifact, type Environment, EnvironmentPlaceholders } from '@aws-cdk/cx-api';
35
import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3';
46
import { getEndpointFromInstructions } from '@smithy/middleware-endpoint';
57
import * as chalk from 'chalk';
6-
import * as fs from 'fs-extra';
78
import { ToolkitError } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api';
8-
import { debug, error } from '../../logging';
9+
import { IO, type IoHelper } from '../../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private';
910
import { contentHash, toYAML } from '../../util';
10-
import { type AssetManifestBuilder } from '../deployments';
11+
import type { AssetManifestBuilder } from '../deployments';
1112
import type { EnvironmentResources } from '../environment';
1213

1314
export type TemplateBodyParameter = {
@@ -31,6 +32,7 @@ const LARGE_TEMPLATE_SIZE_KB = 50;
3132
* @param toolkitInfo information about the toolkit stack
3233
*/
3334
export async function makeBodyParameter(
35+
ioHelper: IoHelper,
3436
stack: CloudFormationStackArtifact,
3537
resolvedEnvironment: Environment,
3638
assetManifest: AssetManifestBuilder,
@@ -53,11 +55,13 @@ export async function makeBodyParameter(
5355

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

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

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

0 commit comments

Comments
 (0)