Skip to content

Commit 802c5c1

Browse files
committed
refactor(toolkit): cxapp to use moden messaging infrastructure
1 parent 0db3dc2 commit 802c5c1

File tree

19 files changed

+334
-242
lines changed

19 files changed

+334
-242
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import type { SharedOptions, DeployOptions, DestroyOptions, BootstrapOptions, SynthOptions, ListOptions } from './commands';
33
import { StackActivityProgress, HotswapMode } from './commands';
44
import { exec as runCli } from '../../../aws-cdk/lib';
5-
// eslint-disable-next-line import/no-extraneous-dependencies
65
import { createAssembly, prepareContext, prepareDefaultEnvironment } from '../../../aws-cdk/lib/api/cxapp/exec';
6+
import { debug } from '../../../aws-cdk/lib/legacy-exports';
7+
// eslint-disable-next-line import/no-extraneous-dependencies
8+
9+
const debugFn = async (msg: string) => void debug(msg);
710

811
/**
912
* AWS CDK CLI operations
@@ -123,8 +126,8 @@ export class AwsCdkCli implements IAwsCdkCli {
123126
public static fromCloudAssemblyDirectoryProducer(producer: ICloudAssemblyDirectoryProducer) {
124127
return new AwsCdkCli(async (args) => changeDir(
125128
() => runCli(args, async (sdk, config) => {
126-
const env = await prepareDefaultEnvironment(sdk);
127-
const context = await prepareContext(config.settings, config.context.all, env);
129+
const env = await prepareDefaultEnvironment(sdk, debugFn);
130+
const context = await prepareContext(config.settings, config.context.all, env, debugFn);
128131

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

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

Lines changed: 8 additions & 0 deletions
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.debug({
387+
code: 'CDK_ASSEMBLY_I0000',
388+
description: 'Default info messages emitted from Cloud Assembly operations',
389+
}),
390+
DEFAULT_ASSEMBLY_WARN: make.debug({
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

Lines changed: 2 additions & 0 deletions
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 | `debug` | n/a |
79+
| `CDK_ASSEMBLY_W0000` | Default warning messages emitted from Cloud Assembly operations | `debug` | 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

Lines changed: 15 additions & 7 deletions
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

Lines changed: 129 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -5,122 +5,151 @@ 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 ExecutionEnviornment {
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();
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;
40+
}
41+
return this._outdir;
42+
}
5243

53-
await logFn('env:', env);
54-
return env;
55-
}
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);
56+
}
5657

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);
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+
if (this.outdir) {
73+
env[cxapi.OUTDIR_ENV] = this.outdir;
74+
await debugFn(format('outdir:', this.outdir));
6575
}
6676

67-
return await block();
68-
} finally {
69-
if (workingDir) {
70-
process.chdir(originalWorkingDir);
71-
}
72-
}
73-
}
77+
// CLI version information
78+
env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version();
79+
env[cxapi.CLI_VERSION_ENV] = versionNumber();
7480

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;
81+
await debugFn(format('env:', env));
82+
return env;
8983
}
90-
}
9184

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;
85+
/**
86+
* Run code from a different working directory
87+
*/
88+
public async changeDir<T>(block: () => Promise<T>, workingDir?: string) {
89+
const originalWorkingDir = process.cwd();
90+
try {
91+
if (workingDir) {
92+
process.chdir(workingDir);
93+
}
94+
95+
return await block();
96+
} finally {
97+
if (workingDir) {
98+
process.chdir(originalWorkingDir);
99+
}
100+
}
101+
}
103102

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;
103+
/**
104+
* Run code with additional environment variables
105+
*/
106+
public async withEnv<T>(env: Env = {}, block: () => Promise<T>) {
107+
const originalEnv = process.env;
108+
try {
109+
process.env = {
110+
...originalEnv,
111+
...env,
112+
};
113+
114+
return await block();
115+
} finally {
116+
process.env = originalEnv;
117117
}
118+
}
118119

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));
120+
/**
121+
* Run code with context setup inside the environment
122+
*/
123+
public async withContext<T>(
124+
inputContext: Context,
125+
env: Env,
126+
synthOpts: AppSynthOptions = {},
127+
block: (env: Env, context: Context) => Promise<T>,
128+
) {
129+
const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env, this.debugFn);
130+
let contextOverflowLocation = null;
131+
132+
try {
133+
const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072;
134+
const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit));
135+
136+
// Store the safe part in the environment variable
137+
env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext);
138+
139+
// If there was any overflow, write it to a temporary file
140+
if (Object.keys(overflow ?? {}).length > 0) {
141+
const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context'));
142+
contextOverflowLocation = path.join(contextDir, 'context-overflow.json');
143+
fs.writeJSONSync(contextOverflowLocation, overflow);
144+
env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation;
145+
}
146+
147+
// call the block code with new environment
148+
return await block(env, context);
149+
} finally {
150+
if (contextOverflowLocation) {
151+
fs.removeSync(path.dirname(contextOverflowLocation));
152+
}
124153
}
125154
}
126155
}
@@ -130,8 +159,9 @@ export async function withContext<T>(
130159
*
131160
* @param assembly the assembly to check
132161
*/
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)));
162+
async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> {
163+
const traceFn = (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg));
164+
const tree = await loadTree(assembly, traceFn);
135165
const frameworkDoesNotSupportContextOverflow = some(tree, node => {
136166
const fqn = node.constructInfo?.fqn;
137167
const version = node.constructInfo?.version;
@@ -149,22 +179,22 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly,
149179
/**
150180
* Safely create an assembly from a cloud assembly directory
151181
*/
152-
export async function assemblyFromDirectory(assemblyDir: string, ioHost: IoHelper, loadOptions: LoadAssemblyOptions = {}) {
182+
export async function assemblyFromDirectory(assemblyDir: string, ioHelper: IoHelper, loadOptions: LoadAssemblyOptions = {}) {
153183
try {
154184
const assembly = new cxapi.CloudAssembly(assemblyDir, {
155185
skipVersionCheck: !(loadOptions.checkVersion ?? true),
156186
skipEnumCheck: !(loadOptions.checkEnums ?? true),
157187
// We sort as we deploy
158188
topoSort: false,
159189
});
160-
await checkContextOverflowSupport(assembly, ioHost);
190+
await checkContextOverflowSupport(assembly, ioHelper);
161191
return assembly;
162192
} catch (err: any) {
163193
if (err.message.includes(cxschema.VERSION_MISMATCH)) {
164194
// this means the CLI version is too old.
165195
// we instruct the user to upgrade.
166196
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 }));
197+
await ioHelper.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err }));
168198
throw new ToolkitError(`${message}\n(${err.message}`);
169199
}
170200
throw err;

0 commit comments

Comments
 (0)