Skip to content

Commit 2a9ee4c

Browse files
mrgraingithub-actions
and
github-actions
authored
refactor(toolkit): cxapp to use moden messaging infrastructure (#285)
Refactors the `cxapp` api code to use modern messaging. This had a larger amount of knock-on effects then other refactors, to the PR is a bit bigger. --- 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]>
1 parent 0db3dc2 commit 2a9ee4c

File tree

19 files changed

+331
-243
lines changed

19 files changed

+331
-243
lines changed

packages/@aws-cdk/cli-lib-alpha/lib/cli.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
// eslint-disable-next-line import/no-extraneous-dependencies
21
import type { SharedOptions, DeployOptions, DestroyOptions, BootstrapOptions, SynthOptions, ListOptions } from './commands';
32
import { StackActivityProgress, HotswapMode } from './commands';
43
import { exec as runCli } from '../../../aws-cdk/lib';
5-
// eslint-disable-next-line import/no-extraneous-dependencies
64
import { createAssembly, prepareContext, prepareDefaultEnvironment } from '../../../aws-cdk/lib/api/cxapp/exec';
5+
import { debug } from '../../../aws-cdk/lib/legacy-exports';
6+
7+
const debugFn = async (msg: string) => void debug(msg);
78

89
/**
910
* AWS CDK CLI operations
@@ -123,8 +124,8 @@ export class AwsCdkCli implements IAwsCdkCli {
123124
public static fromCloudAssemblyDirectoryProducer(producer: ICloudAssemblyDirectoryProducer) {
124125
return new AwsCdkCli(async (args) => changeDir(
125126
() => runCli(args, async (sdk, config) => {
126-
const env = await prepareDefaultEnvironment(sdk);
127-
const context = await prepareContext(config.settings, config.context.all, env);
127+
const env = await prepareDefaultEnvironment(sdk, debugFn);
128+
const context = await prepareContext(config.settings, config.context.all, env, debugFn);
128129

129130
return withEnv(async() => createAssembly(await producer.produce(context)), env);
130131
}),

packages/@aws-cdk/tmp-toolkit-helpers/src/api/io/private/messages.ts

+8
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,14 @@ export const IO = {
383383
code: 'CDK_ASSEMBLY_I0000',
384384
description: 'Default debug messages emitted from Cloud Assembly operations',
385385
}),
386+
DEFAULT_ASSEMBLY_INFO: make.info({
387+
code: 'CDK_ASSEMBLY_I0000',
388+
description: 'Default info messages emitted from Cloud Assembly operations',
389+
}),
390+
DEFAULT_ASSEMBLY_WARN: make.warn({
391+
code: 'CDK_ASSEMBLY_W0000',
392+
description: 'Default warning messages emitted from Cloud Assembly operations',
393+
}),
386394

387395
CDK_ASSEMBLY_I0010: make.debug({
388396
code: 'CDK_ASSEMBLY_I0010',

packages/@aws-cdk/toolkit-lib/docs/message-registry.md

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ group: Documents
7575
| `CDK_TOOLKIT_I0101` | A notice that is marked as informational | `info` | n/a |
7676
| `CDK_ASSEMBLY_I0000` | Default trace messages emitted from Cloud Assembly operations | `trace` | n/a |
7777
| `CDK_ASSEMBLY_I0000` | Default debug messages emitted from Cloud Assembly operations | `debug` | n/a |
78+
| `CDK_ASSEMBLY_I0000` | Default info messages emitted from Cloud Assembly operations | `info` | n/a |
79+
| `CDK_ASSEMBLY_W0000` | Default warning messages emitted from Cloud Assembly operations | `warn` | n/a |
7880
| `CDK_ASSEMBLY_I0010` | Generic environment preparation debug messages | `debug` | n/a |
7981
| `CDK_ASSEMBLY_W0010` | Emitted if the found framework version does not support context overflow | `warn` | n/a |
8082
| `CDK_ASSEMBLY_I0042` | Writing updated context | `debug` | {@link UpdatedContext} |

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

+15-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type * as cxapi from '@aws-cdk/cx-api';
22
import { environmentsFromDescriptors } from './private';
33
import type { Tag } from '../../api/aws-cdk';
4-
import type { ICloudAssemblySource } from '../../api/cloud-assembly';
4+
import type { ICloudAssemblySource, IIoHost } from '../../api/cloud-assembly';
55
import { ALL_STACKS } from '../../api/cloud-assembly/private';
6+
import { asIoHelper } from '../../api/shared-private';
67
import { assemblyFromSource } from '../../toolkit/private';
78

89
/**
@@ -21,21 +22,28 @@ export class BootstrapEnvironments {
2122
* Create from a cloud assembly source
2223
*/
2324
static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments {
24-
return new BootstrapEnvironments(async () => {
25-
const assembly = await assemblyFromSource(cx);
26-
const stackCollection = assembly.selectStacksV2(ALL_STACKS);
25+
return new BootstrapEnvironments(async (ioHost: IIoHost) => {
26+
const ioHelper = asIoHelper(ioHost, 'bootstrap');
27+
const assembly = await assemblyFromSource(ioHelper, cx);
28+
const stackCollection = await assembly.selectStacksV2(ALL_STACKS);
2729
return stackCollection.stackArtifacts.map(stack => stack.environment);
2830
});
2931
}
3032

31-
private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise<cxapi.Environment[]>)) {
33+
private constructor(private readonly envProvider: cxapi.Environment[] | ((ioHost: IIoHost) => Promise<cxapi.Environment[]>)) {
34+
3235
}
3336

34-
async getEnvironments(): Promise<cxapi.Environment[]> {
37+
/**
38+
* Compute the bootstrap enviornments
39+
*
40+
* @internal
41+
*/
42+
async getEnvironments(ioHost: IIoHost): Promise<cxapi.Environment[]> {
3543
if (Array.isArray(this.envProvider)) {
3644
return this.envProvider;
3745
}
38-
return this.envProvider();
46+
return this.envProvider(ioHost);
3947
}
4048
}
4149

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts

+128-100
Original file line numberDiff line numberDiff line change
@@ -5,122 +5,149 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema';
55
import * as cxapi from '@aws-cdk/cx-api';
66
import * as fs from 'fs-extra';
77
import { lte } from 'semver';
8-
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber } from '../../../api/aws-cdk';
8+
import type { SdkProvider } from '../../../api/aws-cdk';
9+
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber, guessExecutable } from '../../../api/aws-cdk';
910
import { splitBySize } from '../../../private/util';
1011
import type { ToolkitServices } from '../../../toolkit/private';
1112
import { IO } from '../../io/private';
1213
import type { IoHelper } from '../../shared-private';
1314
import { ToolkitError } from '../../shared-public';
1415
import type { AppSynthOptions, LoadAssemblyOptions } from '../source-builder';
1516

16-
export { guessExecutable } from '../../../api/aws-cdk';
17-
1817
type Env = { [key: string]: string };
1918
type Context = { [key: string]: any };
2019

21-
/**
22-
* Turn the given optional output directory into a fixed output directory
23-
*/
24-
export function determineOutputDirectory(outdir?: string) {
25-
return outdir ?? fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out'));
26-
}
27-
28-
/**
29-
* If we don't have region/account defined in context, we fall back to the default SDK behavior
30-
* where region is retrieved from ~/.aws/config and account is based on default credentials provider
31-
* chain and then STS is queried.
32-
*
33-
* This is done opportunistically: for example, if we can't access STS for some reason or the region
34-
* is not configured, the context value will be 'null' and there could failures down the line. In
35-
* some cases, synthesis does not require region/account information at all, so that might be perfectly
36-
* fine in certain scenarios.
37-
*
38-
* @param context The context key/value bash.
39-
*/
40-
export async function prepareDefaultEnvironment(services: ToolkitServices, props: { outdir?: string } = {}): Promise<Env> {
41-
const logFn = (msg: string, ...args: any) => services.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(format(msg, ...args)));
42-
const env = await oldPrepare(services.sdkProvider, logFn);
43-
44-
if (props.outdir) {
45-
env[cxapi.OUTDIR_ENV] = props.outdir;
46-
await logFn('outdir:', props.outdir);
20+
export class ExecutionEnvironment {
21+
private readonly ioHelper: IoHelper;
22+
private readonly sdkProvider: SdkProvider;
23+
private readonly debugFn: (msg: string) => Promise<void>;
24+
private _outdir: string | undefined;
25+
26+
public constructor(services: ToolkitServices, props: { outdir?: string } = {}) {
27+
this.ioHelper = services.ioHelper;
28+
this.sdkProvider = services.sdkProvider;
29+
this.debugFn = (msg: string) => this.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg(msg));
30+
this._outdir = props.outdir;
4731
}
4832

49-
// CLI version information
50-
env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version();
51-
env[cxapi.CLI_VERSION_ENV] = versionNumber();
52-
53-
await logFn('env:', env);
54-
return env;
55-
}
56-
57-
/**
58-
* Run code from a different working directory
59-
*/
60-
export async function changeDir<T>(block: () => Promise<T>, workingDir?: string) {
61-
const originalWorkingDir = process.cwd();
62-
try {
63-
if (workingDir) {
64-
process.chdir(workingDir);
33+
/**
34+
* Turn the given optional output directory into a fixed output directory
35+
*/
36+
public get outdir(): string {
37+
if (!this._outdir) {
38+
const outdir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out'));
39+
this._outdir = outdir;
6540
}
41+
return this._outdir;
42+
}
6643

67-
return await block();
68-
} finally {
69-
if (workingDir) {
70-
process.chdir(originalWorkingDir);
71-
}
44+
/**
45+
* Guess the executable from the command-line argument
46+
*
47+
* Only do this if the file is NOT marked as executable. If it is,
48+
* we'll defer to the shebang inside the file itself.
49+
*
50+
* If we're on Windows, we ALWAYS take the handler, since it's hard to
51+
* verify if registry associations have or have not been set up for this
52+
* file type, so we'll assume the worst and take control.
53+
*/
54+
public guessExecutable(app: string) {
55+
return guessExecutable(app, this.debugFn);
7256
}
73-
}
7457

75-
/**
76-
* Run code with additional environment variables
77-
*/
78-
export async function withEnv<T>(env: Env = {}, block: () => Promise<T>) {
79-
const originalEnv = process.env;
80-
try {
81-
process.env = {
82-
...originalEnv,
83-
...env,
84-
};
85-
86-
return await block();
87-
} finally {
88-
process.env = originalEnv;
58+
/**
59+
* If we don't have region/account defined in context, we fall back to the default SDK behavior
60+
* where region is retrieved from ~/.aws/config and account is based on default credentials provider
61+
* chain and then STS is queried.
62+
*
63+
* This is done opportunistically: for example, if we can't access STS for some reason or the region
64+
* is not configured, the context value will be 'null' and there could failures down the line. In
65+
* some cases, synthesis does not require region/account information at all, so that might be perfectly
66+
* fine in certain scenarios.
67+
*/
68+
public async defaultEnvVars(): Promise<Env> {
69+
const debugFn = (msg: string) => this.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(msg));
70+
const env = await oldPrepare(this.sdkProvider, debugFn);
71+
72+
env[cxapi.OUTDIR_ENV] = this.outdir;
73+
await debugFn(format('outdir:', this.outdir));
74+
75+
// CLI version information
76+
env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version();
77+
env[cxapi.CLI_VERSION_ENV] = versionNumber();
78+
79+
await debugFn(format('env:', env));
80+
return env;
8981
}
90-
}
9182

92-
/**
93-
* Run code with context setup inside the environment
94-
*/
95-
export async function withContext<T>(
96-
inputContext: Context,
97-
env: Env,
98-
synthOpts: AppSynthOptions = {},
99-
block: (env: Env, context: Context) => Promise<T>,
100-
) {
101-
const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env);
102-
let contextOverflowLocation = null;
83+
/**
84+
* Run code from a different working directory
85+
*/
86+
public async changeDir<T>(block: () => Promise<T>, workingDir?: string) {
87+
const originalWorkingDir = process.cwd();
88+
try {
89+
if (workingDir) {
90+
process.chdir(workingDir);
91+
}
92+
93+
return await block();
94+
} finally {
95+
if (workingDir) {
96+
process.chdir(originalWorkingDir);
97+
}
98+
}
99+
}
103100

104-
try {
105-
const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072;
106-
const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit));
107-
108-
// Store the safe part in the environment variable
109-
env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext);
110-
111-
// If there was any overflow, write it to a temporary file
112-
if (Object.keys(overflow ?? {}).length > 0) {
113-
const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context'));
114-
contextOverflowLocation = path.join(contextDir, 'context-overflow.json');
115-
fs.writeJSONSync(contextOverflowLocation, overflow);
116-
env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation;
101+
/**
102+
* Run code with additional environment variables
103+
*/
104+
public async withEnv<T>(env: Env = {}, block: () => Promise<T>) {
105+
const originalEnv = process.env;
106+
try {
107+
process.env = {
108+
...originalEnv,
109+
...env,
110+
};
111+
112+
return await block();
113+
} finally {
114+
process.env = originalEnv;
117115
}
116+
}
118117

119-
// call the block code with new environment
120-
return await block(env, context);
121-
} finally {
122-
if (contextOverflowLocation) {
123-
fs.removeSync(path.dirname(contextOverflowLocation));
118+
/**
119+
* Run code with context setup inside the environment
120+
*/
121+
public async withContext<T>(
122+
inputContext: Context,
123+
env: Env,
124+
synthOpts: AppSynthOptions = {},
125+
block: (env: Env, context: Context) => Promise<T>,
126+
) {
127+
const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env, this.debugFn);
128+
let contextOverflowLocation = null;
129+
130+
try {
131+
const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072;
132+
const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit));
133+
134+
// Store the safe part in the environment variable
135+
env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext);
136+
137+
// If there was any overflow, write it to a temporary file
138+
if (Object.keys(overflow ?? {}).length > 0) {
139+
const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context'));
140+
contextOverflowLocation = path.join(contextDir, 'context-overflow.json');
141+
fs.writeJSONSync(contextOverflowLocation, overflow);
142+
env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation;
143+
}
144+
145+
// call the block code with new environment
146+
return await block(env, context);
147+
} finally {
148+
if (contextOverflowLocation) {
149+
fs.removeSync(path.dirname(contextOverflowLocation));
150+
}
124151
}
125152
}
126153
}
@@ -130,8 +157,9 @@ export async function withContext<T>(
130157
*
131158
* @param assembly the assembly to check
132159
*/
133-
export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> {
134-
const tree = loadTree(assembly, (msg: string) => void ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg)));
160+
async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> {
161+
const traceFn = (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg));
162+
const tree = await loadTree(assembly, traceFn);
135163
const frameworkDoesNotSupportContextOverflow = some(tree, node => {
136164
const fqn = node.constructInfo?.fqn;
137165
const version = node.constructInfo?.version;
@@ -149,22 +177,22 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly,
149177
/**
150178
* Safely create an assembly from a cloud assembly directory
151179
*/
152-
export async function assemblyFromDirectory(assemblyDir: string, ioHost: IoHelper, loadOptions: LoadAssemblyOptions = {}) {
180+
export async function assemblyFromDirectory(assemblyDir: string, ioHelper: IoHelper, loadOptions: LoadAssemblyOptions = {}) {
153181
try {
154182
const assembly = new cxapi.CloudAssembly(assemblyDir, {
155183
skipVersionCheck: !(loadOptions.checkVersion ?? true),
156184
skipEnumCheck: !(loadOptions.checkEnums ?? true),
157185
// We sort as we deploy
158186
topoSort: false,
159187
});
160-
await checkContextOverflowSupport(assembly, ioHost);
188+
await checkContextOverflowSupport(assembly, ioHelper);
161189
return assembly;
162190
} catch (err: any) {
163191
if (err.message.includes(cxschema.VERSION_MISMATCH)) {
164192
// this means the CLI version is too old.
165193
// we instruct the user to upgrade.
166194
const message = 'This AWS CDK Toolkit is not compatible with the AWS CDK library used by your application. Please upgrade to the latest version.';
167-
await ioHost.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err }));
195+
await ioHelper.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err }));
168196
throw new ToolkitError(`${message}\n(${err.message}`);
169197
}
170198
throw err;

0 commit comments

Comments
 (0)