Skip to content
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

chore(toolkit): cloud assemblies #32947

Merged
merged 2 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/@aws-cdk/toolkit/lib/api/cloud-assembly/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './cached-source';
export * from './context-aware-source';
export * from './identity-source';
export * from './stack-assembly';
export * from './stack-selector';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import type { MissingContext } from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import { SdkProvider } from 'aws-cdk/lib/api/aws-auth';
import * as contextproviders from 'aws-cdk/lib/context-providers';
import { debug } from 'aws-cdk/lib/logging';
import { Context, PROJECT_CONTEXT } from 'aws-cdk/lib/settings';
import { ICloudAssemblySource } from './types';
import { ToolkitError } from '../errors';
import { ToolkitError } from '../../errors';
import { ActionAwareIoHost, debug } from '../../io/private';
import { ToolkitServices } from '../../toolkit/private';
import { ICloudAssemblySource } from '../types';

export interface CloudExecutableProps {
export interface ContextAwareCloudAssemblyProps {
/**
* AWS object (used by contextprovider)
* @deprecated context should be moved to the toolkit itself
*/
readonly sdkProvider: SdkProvider;
readonly services: ToolkitServices;

/**
* Application context
Expand Down Expand Up @@ -42,11 +43,13 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource {
private canLookup: boolean;
private context: Context;
private contextFile: string;
private ioHost: ActionAwareIoHost;

constructor(private readonly source: ICloudAssemblySource, private readonly props: CloudExecutableProps) {
constructor(private readonly source: ICloudAssemblySource, private readonly props: ContextAwareCloudAssemblyProps) {
this.canLookup = props.lookups ?? true;
this.context = props.context;
this.contextFile = props.contextFile ?? PROJECT_CONTEXT; // @todo new feature not needed right now
this.ioHost = props.services.ioHost;
}

/**
Expand All @@ -73,19 +76,18 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource {

let tryLookup = true;
if (previouslyMissingKeys && equalSets(missingKeys, previouslyMissingKeys)) {
debug('Not making progress trying to resolve environmental context. Giving up.');
await this.ioHost.notify(debug('Not making progress trying to resolve environmental context. Giving up.'));
tryLookup = false;
}

previouslyMissingKeys = missingKeys;

if (tryLookup) {
debug('Some context information is missing. Fetching...');

await this.ioHost.notify(debug('Some context information is missing. Fetching...'));
await contextproviders.provideContextValues(
assembly.manifest.missing,
this.context,
this.props.sdkProvider,
this.props.services.sdkProvider,
);

// Cache the new context to disk
Expand Down
45 changes: 45 additions & 0 deletions packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as child_process from 'node:child_process';
import { ToolkitError } from '../../errors';

interface ExecOptions {
extraEnv?: { [key: string]: string | undefined };
cwd?: string;
}

/**
* Execute a command and args in a child process
*/
export async function execInChildProcess(commandAndArgs: string, options: ExecOptions = {}) {
return new Promise<void>((ok, fail) => {
// We use a slightly lower-level interface to:
//
// - Pass arguments in an array instead of a string, to get around a
// number of quoting issues introduced by the intermediate shell layer
// (which would be different between Linux and Windows).
//
// - Inherit stderr from controlling terminal. We don't use the captured value
// anyway, and if the subprocess is printing to it for debugging purposes the
// user gets to see it sooner. Plus, capturing doesn't interact nicely with some
// processes like Maven.
const proc = child_process.spawn(commandAndArgs, {
stdio: ['ignore', 'inherit', 'inherit'],
detached: false,
shell: true,
cwd: options.cwd,
env: {
...process.env,
...(options.extraEnv ?? {}),
},
});

proc.on('error', fail);

proc.on('exit', code => {
if (code === 0) {
return ok();
} else {
return fail(new ToolkitError(`Subprocess exited with error ${code}`));
}
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import * as os from 'node:os';
import * as path from 'node:path';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext } from 'aws-cdk/lib/api/cxapp/exec';
import { Settings } from 'aws-cdk/lib/settings';
import { loadTree, some } from 'aws-cdk/lib/tree';
import { splitBySize } from 'aws-cdk/lib/util';
import { versionNumber } from 'aws-cdk/lib/version';
import * as fs from 'fs-extra';
import { lte } from 'semver';
import type { AppSynthOptions } from './source-builder';
import { ToolkitError } from '../../errors';
import { ActionAwareIoHost, asLogger, error, warn } from '../../io/private';
import { ToolkitServices } from '../../toolkit/private';

export { guessExecutable } from 'aws-cdk/lib/api/cxapp/exec';

type Env = { [key: string]: string };
type Context = { [key: string]: any };

/**
* 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 = asLogger(services.ioHost, 'ASSEMBLY').debug;
const env = await oldPrepare(services.sdkProvider, logFn);

if (props.outdir) {
env[cxapi.OUTDIR_ENV] = props.outdir;
await logFn('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);
}

return await block();

} finally {
if (workingDir) {
process.chdir(originalWorkingDir);
}
}
}

/**
* 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;
}
}

/**
* 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;

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));
}
}
}

/**
* Checks if a given assembly supports context overflow, warn otherwise.
*
* @param assembly the assembly to check
*/
export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, ioHost: ActionAwareIoHost): Promise<void> {
const logFn = asLogger(ioHost, 'ASSEMBLY').warn;
const tree = loadTree(assembly);
const frameworkDoesNotSupportContextOverflow = some(tree, node => {
const fqn = node.constructInfo?.fqn;
const version = node.constructInfo?.version;
return (fqn === 'aws-cdk-lib.App' && version != null && lte(version, '2.38.0')) // v2
|| fqn === '@aws-cdk/core.App'; // v1
});

// We're dealing with an old version of the framework here. It is unaware of the temporary
// file, which means that it will ignore the context overflow.
if (frameworkDoesNotSupportContextOverflow) {
await logFn('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.');
}
}

/**
* Safely create an assembly from a cloud assembly directory
*/
export async function assemblyFromDirectory(assemblyDir: string, ioHost: ActionAwareIoHost) {
try {
const assembly = new cxapi.CloudAssembly(assemblyDir, {
// We sort as we deploy
topoSort: false,
});
await checkContextOverflowSupport(assembly, ioHost);
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(error(message, 'CDK_ASSEMBLY_E1111', { error: err.message }));
throw new ToolkitError(`${message}\n(${err.message}`);
}
throw err;
}
}
function synthOptsDefaults(synthOpts: AppSynthOptions = {}): Settings {
return new Settings({
debug: false,
pathMetadata: true,
versionReporting: true,
assetMetadata: true,
assetStaging: true,
...synthOpts,
}, true);
}

Loading
Loading