Skip to content

Commit 0d62d34

Browse files
committed
feat(toolkit): a programmatic toolkit for the AWS CDK
1 parent c7d6fb6 commit 0d62d34

35 files changed

+1574
-202
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,33 @@ export interface DeployOptions extends BaseDeployOptions {
225225
* @default AssetBuildTime.ALL_BEFORE_DEPLOY
226226
*/
227227
readonly assetBuildTime?: AssetBuildTime;
228+
229+
/**
230+
* Change stack watcher output to CI mode.
231+
*
232+
* @deprecated Implement in IoHost instead
233+
*/
234+
readonly ci?: boolean;
235+
}
236+
237+
export function buildParameterMap(parameters?: Map<string, string | undefined>): { [name: string]: { [name: string]: string | undefined } } {
238+
const parameterMap: {
239+
[name: string]: { [name: string]: string | undefined };
240+
} = {};
241+
parameterMap['*'] = {};
242+
243+
const entries = parameters?.entries() ?? [];
244+
for (const [key, value] of entries) {
245+
const [stack, parameter] = key.split(':', 2) as [string, string | undefined];
246+
if (!parameter) {
247+
parameterMap['*'][stack] = value;
248+
} else {
249+
if (!parameterMap[stack]) {
250+
parameterMap[stack] = {};
251+
}
252+
parameterMap[stack][parameter] = value;
253+
}
254+
}
255+
256+
return parameterMap;
228257
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,11 @@ export interface DestroyOptions {
1010
* The arn of the IAM role to use
1111
*/
1212
readonly roleArn?: string;
13+
14+
/**
15+
* Change stack watcher output to CI mode.
16+
*
17+
* @deprecated Implement in IoHost instead
18+
*/
19+
readonly ci?: boolean;
1320
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { StackSelector } from '../types';
2+
3+
export interface CloudFormationDiffOptions {
4+
/**
5+
* Whether to run the diff against the template after the CloudFormation Transforms inside it have been executed
6+
* (as opposed to the original template, the default, which contains the unprocessed Transforms).
7+
*
8+
* @default false
9+
*/
10+
readonly compareAgainstProcessedTemplate?: boolean;
11+
}
12+
13+
export interface ChangeSetDiffOptions extends CloudFormationDiffOptions {
14+
/**
15+
* Enable falling back to template-based diff in case creating the changeset is not possible or results in an error.
16+
*
17+
* Should be used for stacks containing nested stacks or when change set permissions aren't available.
18+
*
19+
* @default true
20+
*/
21+
readonly fallbackToTemplate?: boolean;
22+
23+
/**
24+
* Additional parameters for CloudFormation when creating a diff change set
25+
*
26+
* @default {}
27+
*/
28+
readonly parameters?: { [name: string]: string | undefined };
29+
}
30+
31+
export class DiffMode {
32+
/**
33+
* Use a changeset to compute the diff.
34+
*
35+
* This will create, analyze, and subsequently delete a changeset against the CloudFormation stack.
36+
*/
37+
public static ChangeSet(options: ChangeSetDiffOptions = {}) {}
38+
public static TemplateOnly(options: CloudFormationDiffOptions = {}) {}
39+
public static LocalTemplate(path: string) {}
40+
41+
private constructor(public readonly mode: string) {
42+
43+
}
44+
}
45+
46+
export interface DiffOptions {
47+
/**
48+
* Select the stacks
49+
*/
50+
readonly stacks: StackSelector;
51+
52+
/**
53+
* The mode to create a stack diff.
54+
*
55+
* Use changeset diff for the highest fidelity, including analyze resource replacements.
56+
* In this mode, diff will use the deploy role instead of the lookup role.
57+
*
58+
* Use template-only diff for a faster, less accurate diff that doesn't require
59+
* permissions to create a change-set.
60+
*
61+
* Use local-template diff for a fast, local-only diff that doesn't require
62+
* any permissions or internet access.
63+
*
64+
* @default DiffMode.ChangeSet
65+
*/
66+
readonly mode: DiffMode;
67+
68+
/**
69+
* Strict diff mode
70+
* When enabled, this will not filter out AWS::CDK::Metadata resources, mangled non-ASCII characters, or the CheckBootstrapVersionRule.
71+
*
72+
* @default false
73+
*/
74+
readonly strict?: boolean;
75+
76+
/**
77+
* How many lines of context to show in the diff
78+
*
79+
* @default 3
80+
*/
81+
readonly contextLines?: number;
82+
83+
/**
84+
* Only include broadened security changes in the diff
85+
*
86+
* @default false
87+
*/
88+
readonly securityOnly?: boolean;
89+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { StackSelector } from '../types';
2+
3+
export interface ListOptions {
4+
/**
5+
* Select the stacks
6+
*/
7+
readonly stacks: StackSelector;
8+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,21 @@ export interface SynthOptions {
1212
*/
1313
readonly validateStacks?: boolean;
1414
}
15+
16+
/**
17+
* Remove any template elements that we don't want to show users.
18+
*/
19+
export function obscureTemplate(template: any = {}) {
20+
if (template.Rules) {
21+
// see https://github.com/aws/aws-cdk/issues/17942
22+
if (template.Rules.CheckBootstrapVersion) {
23+
if (Object.keys(template.Rules).length > 1) {
24+
delete template.Rules.CheckBootstrapVersion;
25+
} else {
26+
delete template.Rules;
27+
}
28+
}
29+
}
30+
31+
return template;
32+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
/**
3+
* Options for the default SDK provider
4+
*/
5+
export interface SdkOptions {
6+
/**
7+
* Profile to read from ~/.aws
8+
*
9+
* @default - No profile
10+
*/
11+
readonly profile?: string;
12+
13+
/**
14+
* Proxy address to use
15+
*
16+
* @default No proxy
17+
*/
18+
readonly region?: string;
19+
20+
/**
21+
* HTTP options for SDK
22+
*/
23+
readonly httpOptions?: SdkHttpOptions;
24+
}
25+
26+
/**
27+
* Options for individual SDKs
28+
*/
29+
export interface SdkHttpOptions {
30+
/**
31+
* Proxy address to use
32+
*
33+
* @default No proxy
34+
*/
35+
readonly proxyAddress?: string;
36+
37+
/**
38+
* A path to a certificate bundle that contains a cert to be trusted.
39+
*
40+
* @default No certificate bundle
41+
*/
42+
readonly caBundlePath?: string;
43+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { CloudAssembly } from '@aws-cdk/cx-api';
2+
import { ICloudAssemblySource } from './types';
3+
4+
/**
5+
* A CloudAssemblySource that is caching its result once produced.
6+
*
7+
* Most Toolkit interactions should use a cached source.
8+
* Not caching is relevant when the source changes frequently
9+
* and it is to expensive to predict if the source has changed.
10+
*/
11+
export class CachedCloudAssemblySource implements ICloudAssemblySource {
12+
private source: ICloudAssemblySource;
13+
private cloudAssembly: CloudAssembly | undefined;
14+
15+
public constructor(source: ICloudAssemblySource) {
16+
this.source = source;
17+
}
18+
19+
public async produce(): Promise<CloudAssembly> {
20+
if (!this.cloudAssembly) {
21+
this.cloudAssembly = await this.source.produce();
22+
}
23+
return this.cloudAssembly;
24+
}
25+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { MissingContext } from '@aws-cdk/cloud-assembly-schema';
2+
import * as cxapi from '@aws-cdk/cx-api';
3+
import { SdkProvider } from 'aws-cdk/lib/api/aws-auth';
4+
import * as contextproviders from 'aws-cdk/lib/context-providers';
5+
import { debug } from 'aws-cdk/lib/logging';
6+
import { Context, PROJECT_CONTEXT } from 'aws-cdk/lib/settings';
7+
import { ICloudAssemblySource } from './types';
8+
import { ToolkitError } from '../errors';
9+
10+
export interface CloudExecutableProps {
11+
/**
12+
* AWS object (used by contextprovider)
13+
*/
14+
readonly sdkProvider: SdkProvider;
15+
16+
/**
17+
* Application context
18+
*/
19+
readonly context: Context;
20+
21+
/**
22+
* The file used to store application context in (relative to cwd).
23+
*
24+
* @default "cdk.context.json"
25+
*/
26+
readonly contextFile?: string;
27+
28+
/**
29+
* Enable context lookups.
30+
*
31+
* Producing a `cxapi.CloudAssembly` will fail if this is disabled and context lookups need to be performed.
32+
*
33+
* @default true
34+
*/
35+
readonly lookups?: boolean;
36+
}
37+
38+
/**
39+
* Represent the Cloud Executable and the synthesis we can do on it
40+
*/
41+
export class ContextAwareCloudAssembly implements ICloudAssemblySource {
42+
private canLookup: boolean;
43+
private context: Context;
44+
private contextFile: string;
45+
46+
constructor(private readonly source: ICloudAssemblySource, private readonly props: CloudExecutableProps) {
47+
this.canLookup = props.lookups ?? true;
48+
this.context = props.context;
49+
this.contextFile = props.contextFile ?? PROJECT_CONTEXT; // @todo new feature not needed right now
50+
}
51+
52+
/**
53+
* Produce a Cloud Assembly, i.e. a set of stacks
54+
*/
55+
public async produce(): Promise<cxapi.CloudAssembly> {
56+
// We may need to run the cloud executable multiple times in order to satisfy all missing context
57+
// (When the executable runs, it will tell us about context it wants to use
58+
// but it missing. We'll then look up the context and run the executable again, and
59+
// again, until it doesn't complain anymore or we've stopped making progress).
60+
let previouslyMissingKeys: Set<string> | undefined;
61+
while (true) {
62+
const assembly = await this.source.produce();
63+
64+
if (assembly.manifest.missing && assembly.manifest.missing.length > 0) {
65+
const missingKeys = missingContextKeys(assembly.manifest.missing);
66+
67+
if (!this.canLookup) {
68+
throw new ToolkitError(
69+
'Context lookups have been disabled. '
70+
+ 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. '
71+
+ `Missing context keys: '${Array.from(missingKeys).join(', ')}'`);
72+
}
73+
74+
let tryLookup = true;
75+
if (previouslyMissingKeys && equalSets(missingKeys, previouslyMissingKeys)) {
76+
debug('Not making progress trying to resolve environmental context. Giving up.');
77+
tryLookup = false;
78+
}
79+
80+
previouslyMissingKeys = missingKeys;
81+
82+
if (tryLookup) {
83+
debug('Some context information is missing. Fetching...');
84+
85+
await contextproviders.provideContextValues(
86+
assembly.manifest.missing,
87+
this.context,
88+
this.props.sdkProvider,
89+
);
90+
91+
// Cache the new context to disk
92+
await this.context.save(this.contextFile);
93+
94+
// Execute again
95+
continue;
96+
}
97+
}
98+
99+
return assembly;
100+
}
101+
}
102+
103+
}
104+
105+
/**
106+
* Return all keys of missing context items
107+
*/
108+
function missingContextKeys(missing?: MissingContext[]): Set<string> {
109+
return new Set((missing || []).map(m => m.key));
110+
}
111+
112+
/**
113+
* Are two sets equal to each other
114+
*/
115+
function equalSets<A>(a: Set<A>, b: Set<A>) {
116+
if (a.size !== b.size) { return false; }
117+
for (const x of a) {
118+
if (!b.has(x)) { return false; }
119+
}
120+
return true;
121+
}

0 commit comments

Comments
 (0)