-
Notifications
You must be signed in to change notification settings - Fork 69
refactor(toolkit): cxapp to use moden messaging infrastructure #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| import type * as cxapi from '@aws-cdk/cx-api'; | ||
| import { environmentsFromDescriptors } from './private'; | ||
| import type { Tag } from '../../api/aws-cdk'; | ||
| import type { ICloudAssemblySource } from '../../api/cloud-assembly'; | ||
| import type { ICloudAssemblySource, IIoHost } from '../../api/cloud-assembly'; | ||
| import { ALL_STACKS } from '../../api/cloud-assembly/private'; | ||
| import { asIoHelper } from '../../api/shared-private'; | ||
| import { assemblyFromSource } from '../../toolkit/private'; | ||
|
|
||
| /** | ||
|
|
@@ -21,21 +22,28 @@ export class BootstrapEnvironments { | |
| * Create from a cloud assembly source | ||
| */ | ||
| static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments { | ||
| return new BootstrapEnvironments(async () => { | ||
| const assembly = await assemblyFromSource(cx); | ||
| const stackCollection = assembly.selectStacksV2(ALL_STACKS); | ||
| return new BootstrapEnvironments(async (ioHost: IIoHost) => { | ||
| const ioHelper = asIoHelper(ioHost, 'bootstrap'); | ||
| const assembly = await assemblyFromSource(ioHelper, cx); | ||
| const stackCollection = await assembly.selectStacksV2(ALL_STACKS); | ||
| return stackCollection.stackArtifacts.map(stack => stack.environment); | ||
| }); | ||
| } | ||
|
|
||
| private constructor(private readonly envProvider: cxapi.Environment[] | (() => Promise<cxapi.Environment[]>)) { | ||
| private constructor(private readonly envProvider: cxapi.Environment[] | ((ioHost: IIoHost) => Promise<cxapi.Environment[]>)) { | ||
|
|
||
| } | ||
|
|
||
| async getEnvironments(): Promise<cxapi.Environment[]> { | ||
| /** | ||
| * Compute the bootstrap enviornments | ||
| * | ||
| * @internal | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was previously missed to mark internal. |
||
| */ | ||
| async getEnvironments(ioHost: IIoHost): Promise<cxapi.Environment[]> { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it's internal, we can change this. |
||
| if (Array.isArray(this.envProvider)) { | ||
| return this.envProvider; | ||
| } | ||
| return this.envProvider(); | ||
| return this.envProvider(ioHost); | ||
| } | ||
| } | ||
|
|
||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this turned into a bigger refactor. I realized a lot of these helpers share the same context, so a new abstraction (class) seemed appropriate. No functional changes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,122 +5,149 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; | |
| import * as cxapi from '@aws-cdk/cx-api'; | ||
| import * as fs from 'fs-extra'; | ||
| import { lte } from 'semver'; | ||
| import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber } from '../../../api/aws-cdk'; | ||
| import type { SdkProvider } from '../../../api/aws-cdk'; | ||
| import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, Settings, loadTree, some, versionNumber, guessExecutable } from '../../../api/aws-cdk'; | ||
| import { splitBySize } from '../../../private/util'; | ||
| import type { ToolkitServices } from '../../../toolkit/private'; | ||
| import { IO } from '../../io/private'; | ||
| import type { IoHelper } from '../../shared-private'; | ||
| import { ToolkitError } from '../../shared-public'; | ||
| import type { AppSynthOptions, LoadAssemblyOptions } from '../source-builder'; | ||
|
|
||
| export { guessExecutable } from '../../../api/aws-cdk'; | ||
|
|
||
| type Env = { [key: string]: string }; | ||
| type Context = { [key: string]: any }; | ||
|
|
||
| /** | ||
| * Turn the given optional output directory into a fixed output directory | ||
| */ | ||
| export function determineOutputDirectory(outdir?: string) { | ||
| return outdir ?? fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out')); | ||
| } | ||
|
|
||
| /** | ||
| * If we don't have region/account defined in context, we fall back to the default SDK behavior | ||
| * where region is retrieved from ~/.aws/config and account is based on default credentials provider | ||
| * chain and then STS is queried. | ||
| * | ||
| * This is done opportunistically: for example, if we can't access STS for some reason or the region | ||
| * is not configured, the context value will be 'null' and there could failures down the line. In | ||
| * some cases, synthesis does not require region/account information at all, so that might be perfectly | ||
| * fine in certain scenarios. | ||
| * | ||
| * @param context The context key/value bash. | ||
| */ | ||
| export async function prepareDefaultEnvironment(services: ToolkitServices, props: { outdir?: string } = {}): Promise<Env> { | ||
| const logFn = (msg: string, ...args: any) => services.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(format(msg, ...args))); | ||
| const env = await oldPrepare(services.sdkProvider, logFn); | ||
|
|
||
| if (props.outdir) { | ||
| env[cxapi.OUTDIR_ENV] = props.outdir; | ||
| await logFn('outdir:', props.outdir); | ||
| export class ExecutionEnvironment { | ||
| private readonly ioHelper: IoHelper; | ||
| private readonly sdkProvider: SdkProvider; | ||
| private readonly debugFn: (msg: string) => Promise<void>; | ||
| private _outdir: string | undefined; | ||
|
|
||
| public constructor(services: ToolkitServices, props: { outdir?: string } = {}) { | ||
| this.ioHelper = services.ioHelper; | ||
| this.sdkProvider = services.sdkProvider; | ||
| this.debugFn = (msg: string) => this.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg(msg)); | ||
| this._outdir = props.outdir; | ||
| } | ||
|
|
||
| // CLI version information | ||
| env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); | ||
| env[cxapi.CLI_VERSION_ENV] = versionNumber(); | ||
|
|
||
| await logFn('env:', env); | ||
| return env; | ||
| } | ||
|
|
||
| /** | ||
| * Run code from a different working directory | ||
| */ | ||
| export async function changeDir<T>(block: () => Promise<T>, workingDir?: string) { | ||
| const originalWorkingDir = process.cwd(); | ||
| try { | ||
| if (workingDir) { | ||
| process.chdir(workingDir); | ||
| /** | ||
| * Turn the given optional output directory into a fixed output directory | ||
| */ | ||
| public get outdir(): string { | ||
| if (!this._outdir) { | ||
| const outdir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk.out')); | ||
| this._outdir = outdir; | ||
| } | ||
| return this._outdir; | ||
| } | ||
|
|
||
| return await block(); | ||
| } finally { | ||
| if (workingDir) { | ||
| process.chdir(originalWorkingDir); | ||
| } | ||
| /** | ||
| * Guess the executable from the command-line argument | ||
| * | ||
| * Only do this if the file is NOT marked as executable. If it is, | ||
| * we'll defer to the shebang inside the file itself. | ||
| * | ||
| * If we're on Windows, we ALWAYS take the handler, since it's hard to | ||
| * verify if registry associations have or have not been set up for this | ||
| * file type, so we'll assume the worst and take control. | ||
| */ | ||
| public guessExecutable(app: string) { | ||
| return guessExecutable(app, this.debugFn); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Run code with additional environment variables | ||
| */ | ||
| export async function withEnv<T>(env: Env = {}, block: () => Promise<T>) { | ||
| const originalEnv = process.env; | ||
| try { | ||
| process.env = { | ||
| ...originalEnv, | ||
| ...env, | ||
| }; | ||
|
|
||
| return await block(); | ||
| } finally { | ||
| process.env = originalEnv; | ||
| /** | ||
| * If we don't have region/account defined in context, we fall back to the default SDK behavior | ||
| * where region is retrieved from ~/.aws/config and account is based on default credentials provider | ||
| * chain and then STS is queried. | ||
| * | ||
| * This is done opportunistically: for example, if we can't access STS for some reason or the region | ||
| * is not configured, the context value will be 'null' and there could failures down the line. In | ||
| * some cases, synthesis does not require region/account information at all, so that might be perfectly | ||
| * fine in certain scenarios. | ||
| */ | ||
| public async defaultEnvVars(): Promise<Env> { | ||
| const debugFn = (msg: string) => this.ioHelper.notify(IO.CDK_ASSEMBLY_I0010.msg(msg)); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. uses a different code than the other messages in this class. probably doesn't need to. We can make a breaking change at some point. |
||
| const env = await oldPrepare(this.sdkProvider, debugFn); | ||
|
|
||
| env[cxapi.OUTDIR_ENV] = this.outdir; | ||
| await debugFn(format('outdir:', this.outdir)); | ||
|
|
||
| // CLI version information | ||
| env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); | ||
| env[cxapi.CLI_VERSION_ENV] = versionNumber(); | ||
|
|
||
| await debugFn(format('env:', env)); | ||
| return env; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Run code with context setup inside the environment | ||
| */ | ||
| export async function withContext<T>( | ||
| inputContext: Context, | ||
| env: Env, | ||
| synthOpts: AppSynthOptions = {}, | ||
| block: (env: Env, context: Context) => Promise<T>, | ||
| ) { | ||
| const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env); | ||
| let contextOverflowLocation = null; | ||
| /** | ||
| * Run code from a different working directory | ||
| */ | ||
| public async changeDir<T>(block: () => Promise<T>, workingDir?: string) { | ||
| const originalWorkingDir = process.cwd(); | ||
| try { | ||
| if (workingDir) { | ||
| process.chdir(workingDir); | ||
| } | ||
|
|
||
| return await block(); | ||
| } finally { | ||
| if (workingDir) { | ||
| process.chdir(originalWorkingDir); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; | ||
| const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); | ||
|
|
||
| // Store the safe part in the environment variable | ||
| env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext); | ||
|
|
||
| // If there was any overflow, write it to a temporary file | ||
| if (Object.keys(overflow ?? {}).length > 0) { | ||
| const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context')); | ||
| contextOverflowLocation = path.join(contextDir, 'context-overflow.json'); | ||
| fs.writeJSONSync(contextOverflowLocation, overflow); | ||
| env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation; | ||
| /** | ||
| * Run code with additional environment variables | ||
| */ | ||
| public async withEnv<T>(env: Env = {}, block: () => Promise<T>) { | ||
| const originalEnv = process.env; | ||
| try { | ||
| process.env = { | ||
| ...originalEnv, | ||
| ...env, | ||
| }; | ||
|
|
||
| return await block(); | ||
| } finally { | ||
| process.env = originalEnv; | ||
| } | ||
| } | ||
|
|
||
| // call the block code with new environment | ||
| return await block(env, context); | ||
| } finally { | ||
| if (contextOverflowLocation) { | ||
| fs.removeSync(path.dirname(contextOverflowLocation)); | ||
| /** | ||
| * Run code with context setup inside the environment | ||
| */ | ||
| public async withContext<T>( | ||
| inputContext: Context, | ||
| env: Env, | ||
| synthOpts: AppSynthOptions = {}, | ||
| block: (env: Env, context: Context) => Promise<T>, | ||
| ) { | ||
| const context = await prepareContext(synthOptsDefaults(synthOpts), inputContext, env, this.debugFn); | ||
| let contextOverflowLocation = null; | ||
|
|
||
| try { | ||
| const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; | ||
| const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); | ||
|
|
||
| // Store the safe part in the environment variable | ||
| env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext); | ||
|
|
||
| // If there was any overflow, write it to a temporary file | ||
| if (Object.keys(overflow ?? {}).length > 0) { | ||
| const contextDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-context')); | ||
| contextOverflowLocation = path.join(contextDir, 'context-overflow.json'); | ||
| fs.writeJSONSync(contextOverflowLocation, overflow); | ||
| env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation; | ||
| } | ||
|
|
||
| // call the block code with new environment | ||
| return await block(env, context); | ||
| } finally { | ||
| if (contextOverflowLocation) { | ||
| fs.removeSync(path.dirname(contextOverflowLocation)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -130,8 +157,9 @@ export async function withContext<T>( | |
| * | ||
| * @param assembly the assembly to check | ||
| */ | ||
| export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> { | ||
| const tree = loadTree(assembly, (msg: string) => void ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg))); | ||
| async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHelper: IoHelper): Promise<void> { | ||
| const traceFn = (msg: string) => ioHelper.notify(IO.DEFAULT_ASSEMBLY_TRACE.msg(msg)); | ||
| const tree = await loadTree(assembly, traceFn); | ||
| const frameworkDoesNotSupportContextOverflow = some(tree, node => { | ||
| const fqn = node.constructInfo?.fqn; | ||
| const version = node.constructInfo?.version; | ||
|
|
@@ -149,22 +177,22 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, | |
| /** | ||
| * Safely create an assembly from a cloud assembly directory | ||
| */ | ||
| export async function assemblyFromDirectory(assemblyDir: string, ioHost: IoHelper, loadOptions: LoadAssemblyOptions = {}) { | ||
| export async function assemblyFromDirectory(assemblyDir: string, ioHelper: IoHelper, loadOptions: LoadAssemblyOptions = {}) { | ||
| try { | ||
| const assembly = new cxapi.CloudAssembly(assemblyDir, { | ||
| skipVersionCheck: !(loadOptions.checkVersion ?? true), | ||
| skipEnumCheck: !(loadOptions.checkEnums ?? true), | ||
| // We sort as we deploy | ||
| topoSort: false, | ||
| }); | ||
| await checkContextOverflowSupport(assembly, ioHost); | ||
| await checkContextOverflowSupport(assembly, ioHelper); | ||
| return assembly; | ||
| } catch (err: any) { | ||
| if (err.message.includes(cxschema.VERSION_MISMATCH)) { | ||
| // this means the CLI version is too old. | ||
| // we instruct the user to upgrade. | ||
| const message = 'This AWS CDK Toolkit is not compatible with the AWS CDK library used by your application. Please upgrade to the latest version.'; | ||
| await ioHost.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err })); | ||
| await ioHelper.notify(IO.CDK_ASSEMBLY_E1111.msg(message, { error: err })); | ||
| throw new ToolkitError(`${message}\n(${err.message}`); | ||
| } | ||
| throw err; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the default value anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prepareDefaultEnvironmentused to have a default fordebugFn, but the default was using the old legacy logging system. Removed the default, made the param mandatory.cli-lib-alphawill use legacy logging until further notice, but we plan to deprecate that package anyway soon.