Skip to content

Commit faa2de0

Browse files
iankhougithub-actionsmrgrain
authored
feat(toolkit-lib): bootstrap action (#63)
Fixes aws/aws-cdk#33180. Typed return will be included in a follow-up PR. Description: Bootstrap Action for the CDK Toolkit. Testing: - Unit test coverage for: - Bootstrap stack creation - No-op (no stack changes) - Generic bootstrap error - Permissions error during bootstrap - Bootstrapped an environment in a test account using this code --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions <[email protected]> Co-authored-by: github-actions <[email protected]> Co-authored-by: Momo Kornher <[email protected]>
1 parent 7c65899 commit faa2de0

File tree

20 files changed

+1029
-39
lines changed

20 files changed

+1029
-39
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/util/directories.ts

+28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs';
22
import * as os from 'os';
33
import * as path from 'path';
4+
import { ToolkitError } from '../api/toolkit-error';
45

56
/**
67
* Return a location that will be used as the CDK home directory.
@@ -34,3 +35,30 @@ export function cdkHomeDir() {
3435
export function cdkCacheDir() {
3536
return path.join(cdkHomeDir(), 'cache');
3637
}
38+
39+
/**
40+
* From the start location, find the directory that contains the bundled package's package.json
41+
*
42+
* You must assume the caller of this function will be bundled and the package root dir
43+
* is not going to be the same as the package the caller currently lives in.
44+
*/
45+
export function bundledPackageRootDir(start: string): string;
46+
export function bundledPackageRootDir(start: string, fail: true): string;
47+
export function bundledPackageRootDir(start: string, fail: false): string | undefined;
48+
export function bundledPackageRootDir(start: string, fail?: boolean) {
49+
function _rootDir(dirname: string): string | undefined {
50+
const manifestPath = path.join(dirname, 'package.json');
51+
if (fs.existsSync(manifestPath)) {
52+
return dirname;
53+
}
54+
if (path.dirname(dirname) === dirname) {
55+
if (fail ?? true) {
56+
throw new ToolkitError('Unable to find package manifest');
57+
}
58+
return undefined;
59+
}
60+
return _rootDir(path.dirname(dirname));
61+
}
62+
63+
return _rootDir(start);
64+
}

packages/@aws-cdk/toolkit-lib/CODE_REGISTRY.md

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
| CDK_TOOLKIT_I7010 | Confirm destroy stacks | info | n/a |
2727
| CDK_TOOLKIT_E7010 | Action was aborted due to negative confirmation of request | error | n/a |
2828
| CDK_TOOLKIT_E7900 | Stack deletion failed | error | n/a |
29+
| CDK_TOOLKIT_I9000 | Provides bootstrap times | info | n/a |
30+
| CDK_TOOLKIT_I9900 | Bootstrap results on success | info | n/a |
31+
| CDK_TOOLKIT_E9900 | Bootstrap failed | error | n/a |
2932
| CDK_ASSEMBLY_I0042 | Writing updated context | debug | n/a |
3033
| CDK_ASSEMBLY_I0241 | Fetching missing context | debug | n/a |
3134
| CDK_ASSEMBLY_I1000 | Cloud assembly output starts | debug | n/a |

packages/@aws-cdk/toolkit-lib/build-tools/bundle.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ await Promise.all([
1616
copyFromCli(['build-info.json']),
1717
copyFromCli(['/db.json.gz']),
1818
copyFromCli(['lib', 'index_bg.wasm']),
19+
copyFromCli(['lib', 'api', 'bootstrap', 'bootstrap-template.yaml']),
1920
]);
2021

2122
// # Copy all resources that aws_cdk/generate.sh produced, and some othersCall the generator for the
2223
// cp -R $aws_cdk/lib/init-templates ./lib/
23-
// mkdir -p ./lib/api/bootstrap/ && cp $aws_cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/
2424

2525
await esbuild.build({
2626
outdir: 'lib',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import * as cxapi from '@aws-cdk/cx-api';
2+
import { environmentsFromDescriptors } from './private';
3+
import type { Tag } from '../../api/aws-cdk';
4+
import type { ICloudAssemblySource } from '../../api/cloud-assembly';
5+
import { ALL_STACKS } from '../../api/cloud-assembly/private';
6+
import { assemblyFromSource } from '../../toolkit/private';
7+
8+
/**
9+
* Create manage bootstrap environments
10+
*/
11+
export class BootstrapEnvironments {
12+
/**
13+
* Create from a list of environment descriptors
14+
* List of strings like `['aws://012345678912/us-east-1', 'aws://234567890123/eu-west-1']`
15+
*/
16+
static fromList(environments: string[]): BootstrapEnvironments {
17+
return new BootstrapEnvironments(environmentsFromDescriptors(environments));
18+
}
19+
20+
/**
21+
* Create from a cloud assembly source
22+
*/
23+
static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments {
24+
return new BootstrapEnvironments(async () => {
25+
const assembly = await assemblyFromSource(cx);
26+
const stackCollection = assembly.selectStacksV2(ALL_STACKS);
27+
return stackCollection.stackArtifacts.map(stack => stack.environment);
28+
});
29+
}
30+
31+
private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise<cxapi.Environment[]>)) {
32+
}
33+
34+
async getEnvironments(): Promise<cxapi.Environment[]> {
35+
if (Array.isArray(this.envProvider)) {
36+
return this.envProvider;
37+
}
38+
return this.envProvider();
39+
}
40+
}
41+
42+
/**
43+
* Options for Bootstrap
44+
*/
45+
export interface BootstrapOptions {
46+
47+
/**
48+
* Bootstrap environment parameters for CloudFormation used when deploying the bootstrap stack
49+
* @default BootstrapEnvironmentParameters.onlyExisting()
50+
*/
51+
readonly parameters?: BootstrapStackParameters;
52+
53+
/**
54+
* The template source of the bootstrap stack
55+
*
56+
* @default BootstrapSource.default()
57+
*/
58+
readonly source?: { source: 'default' } | { source: 'custom'; templateFile: string };
59+
60+
/**
61+
* Whether to execute the changeset or only create it and leave it in review
62+
* @default true
63+
*/
64+
readonly execute?: boolean;
65+
66+
/**
67+
* Tags for cdktoolkit stack
68+
*
69+
* @default []
70+
*/
71+
readonly tags?: Tag[];
72+
73+
/**
74+
* Whether the stacks created by the bootstrap process should be protected from termination
75+
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html
76+
* @default true
77+
*/
78+
readonly terminationProtection?: boolean;
79+
}
80+
81+
/**
82+
* Parameter values for the bootstrapping template
83+
*/
84+
export interface BootstrapParameters {
85+
/**
86+
* The name to be given to the CDK Bootstrap bucket
87+
* By default, a name is generated by CloudFormation
88+
*
89+
* @default - No value, optional argument
90+
*/
91+
readonly bucketName?: string;
92+
93+
/**
94+
* The ID of an existing KMS key to be used for encrypting items in the bucket
95+
* By default, the default KMS key is used
96+
*
97+
* @default - No value, optional argument
98+
*/
99+
readonly kmsKeyId?: string;
100+
101+
/**
102+
* Whether or not to create a new customer master key (CMK)
103+
*
104+
* Only applies to modern bootstrapping
105+
* Legacy bootstrapping will never create a CMK, only use the default S3 key
106+
*
107+
* @default false
108+
*/
109+
readonly createCustomerMasterKey?: boolean;
110+
111+
/**
112+
* The list of AWS account IDs that are trusted to deploy into the environment being bootstrapped
113+
*
114+
* @default []
115+
*/
116+
readonly trustedAccounts?: string[];
117+
118+
/**
119+
* The list of AWS account IDs that are trusted to look up values in the environment being bootstrapped
120+
*
121+
* @default []
122+
*/
123+
readonly trustedAccountsForLookup?: string[];
124+
125+
/**
126+
* The list of AWS account IDs that should not be trusted by the bootstrapped environment
127+
* If these accounts are already trusted, they will be removed on bootstrapping
128+
*
129+
* @default []
130+
*/
131+
readonly untrustedAccounts?: string[];
132+
133+
/**
134+
* The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments
135+
* In most cases, this will be the AdministratorAccess policy
136+
* At least one policy is required if `trustedAccounts` were passed
137+
*
138+
* @default []
139+
*/
140+
readonly cloudFormationExecutionPolicies?: string[];
141+
142+
/**
143+
* Identifier to distinguish multiple bootstrapped environments
144+
* The default qualifier is an arbitrary but unique string
145+
*
146+
* @default - 'hnb659fds'
147+
*/
148+
readonly qualifier?: string;
149+
150+
/**
151+
* Whether or not to enable S3 Staging Bucket Public Access Block Configuration
152+
*
153+
* @default true
154+
*/
155+
readonly publicAccessBlockConfiguration?: boolean;
156+
157+
/**
158+
* Flag for using the default permissions boundary for bootstrapping
159+
*
160+
* @default - No value, optional argument
161+
*/
162+
readonly examplePermissionsBoundary?: boolean;
163+
164+
/**
165+
* Name for the customer's custom permissions boundary for bootstrapping
166+
*
167+
* @default - No value, optional argument
168+
*/
169+
readonly customPermissionsBoundary?: string;
170+
}
171+
172+
/**
173+
* Parameters of the bootstrapping template with flexible configuration options
174+
*/
175+
export class BootstrapStackParameters {
176+
/**
177+
* Use only existing parameters on the stack.
178+
*/
179+
public static onlyExisting() {
180+
return new BootstrapStackParameters({}, true);
181+
}
182+
183+
/**
184+
* Use exactly these parameters and remove any other existing parameters from the stack.
185+
*/
186+
public static exactly(params: BootstrapParameters) {
187+
return new BootstrapStackParameters(params, false);
188+
}
189+
190+
/**
191+
* Define additional parameters for the stack, while keeping existing parameters for unspecified values.
192+
*/
193+
public static withExisting(params: BootstrapParameters) {
194+
return new BootstrapStackParameters(params, true);
195+
}
196+
197+
/**
198+
* The parameters as a Map for easy access and manipulation
199+
*/
200+
public readonly parameters?: BootstrapParameters;
201+
public readonly keepExistingParameters: boolean;
202+
203+
private constructor(params?: BootstrapParameters, usePreviousParameters = true) {
204+
this.keepExistingParameters = usePreviousParameters;
205+
this.parameters = params;
206+
}
207+
}
208+
209+
/**
210+
* Source configuration for bootstrap operations
211+
*/
212+
export class BootstrapSource {
213+
/**
214+
* Use the default bootstrap template
215+
*/
216+
static default(): BootstrapOptions['source'] {
217+
return { source: 'default' };
218+
}
219+
220+
/**
221+
* Use a custom bootstrap template
222+
*/
223+
static customTemplate(templateFile: string): BootstrapOptions['source'] {
224+
return {
225+
source: 'custom',
226+
templateFile,
227+
};
228+
}
229+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as cxapi from '@aws-cdk/cx-api';
2+
import { ToolkitError } from '../../../api/shared-public';
3+
4+
/**
5+
* Given a set of "<account>/<region>" strings, construct environments for them
6+
*/
7+
export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environment[] {
8+
const ret = new Array<cxapi.Environment>();
9+
10+
for (const spec of envSpecs) {
11+
const parts = spec.replace(/^aws:\/\//, '').split('/');
12+
if (parts.length !== 2) {
13+
throw new ToolkitError(`Expected environment name in format 'aws://<account>/<region>', got: ${spec}`);
14+
}
15+
16+
ret.push({
17+
name: spec,
18+
account: parts[0],
19+
region: parts[1],
20+
});
21+
}
22+
23+
return ret;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './helpers';

packages/@aws-cdk/toolkit-lib/lib/actions/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './bootstrap';
12
export * from './deploy';
23
export * from './destroy';
34
export * from './list';

packages/@aws-cdk/toolkit-lib/lib/api/aws-cdk.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ export { formatSdkLoggerContent, SdkProvider } from '../../../../aws-cdk/lib/api
55
export { Context, PROJECT_CONTEXT } from '../../../../aws-cdk/lib/api/context';
66
export { Deployments, type SuccessfulDeployStackResult } from '../../../../aws-cdk/lib/api/deployments';
77
export { Settings } from '../../../../aws-cdk/lib/api/settings';
8-
export { tagsForStack, Tag } from '../../../../aws-cdk/lib/api/tags';
8+
export { type Tag, tagsForStack } from '../../../../aws-cdk/lib/api/tags';
99
export { DEFAULT_TOOLKIT_STACK_NAME } from '../../../../aws-cdk/lib/api/toolkit-info';
1010
export { ResourceMigrator } from '../../../../aws-cdk/lib/api/resource-import';
1111
export { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../../../../aws-cdk/lib/api/logs';
1212
export { type WorkGraph, WorkGraphBuilder, AssetBuildNode, AssetPublishNode, StackNode, Concurrency } from '../../../../aws-cdk/lib/api/work-graph';
13+
export { Bootstrapper } from '../../../../aws-cdk/lib/api/bootstrap';
1314

1415
// Context Providers
1516
export * as contextproviders from '../../../../aws-cdk/lib/context-providers';

packages/@aws-cdk/toolkit-lib/lib/api/io/private/codes.ts

+15
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,21 @@ export const CODES = {
190190
}),
191191

192192
// 9: Bootstrap
193+
CDK_TOOLKIT_I9000: codeInfo({
194+
code: 'CDK_TOOLKIT_I9000',
195+
description: 'Provides bootstrap times',
196+
level: 'info',
197+
}),
198+
CDK_TOOLKIT_I9900: codeInfo({
199+
code: 'CDK_TOOLKIT_I9900',
200+
description: 'Bootstrap results on success',
201+
level: 'info',
202+
}),
203+
CDK_TOOLKIT_E9900: codeInfo({
204+
code: 'CDK_TOOLKIT_E9900',
205+
description: 'Bootstrap failed',
206+
level: 'error',
207+
}),
193208

194209
// Assembly codes
195210
CDK_ASSEMBLY_I0042: codeInfo({

packages/@aws-cdk/toolkit-lib/lib/api/io/private/timer.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class Timer {
3737
* Ends the current timer as a specified timing and notifies the IoHost.
3838
* @returns the elapsed time
3939
*/
40-
public async endAs(ioHost: ActionAwareIoHost, type: 'synth' | 'deploy' | 'rollback' | 'destroy') {
40+
public async endAs(ioHost: ActionAwareIoHost, type: 'synth' | 'deploy' | 'rollback' | 'destroy' | 'bootstrap') {
4141
const duration = this.end();
4242
const { code, text } = timerMessageProps(type);
4343

@@ -49,7 +49,7 @@ export class Timer {
4949
}
5050
}
5151

52-
function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy'): {
52+
function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy' | 'bootstrap'): {
5353
code: CodeInfo;
5454
text: string;
5555
} {
@@ -58,5 +58,6 @@ function timerMessageProps(type: 'synth' | 'deploy' | 'rollback'| 'destroy'): {
5858
case 'deploy': return { code: CODES.CDK_TOOLKIT_I5000, text: 'Deployment' };
5959
case 'rollback': return { code: CODES.CDK_TOOLKIT_I6000, text: 'Rollback' };
6060
case 'destroy': return { code: CODES.CDK_TOOLKIT_I7000, text: 'Destroy' };
61+
case 'bootstrap': return { code: CODES.CDK_TOOLKIT_I9000, text: 'Bootstrap' };
6162
}
6263
}

0 commit comments

Comments
 (0)