From a5226acda71488201ee8e1252b207392e3f4c387 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 18 Nov 2025 16:19:32 +0100 Subject: [PATCH 1/4] fix(integ-runner): region is not passed to CDK App When using the `toolkit-lib` deployment engine, the selected region was not being passed to the CDK app. In the old engine, we could set the `AWS_REGION` environment variable and the CDK CLI would respect that, but the toolkit lib was passing that environment variable directly to the CDK app which was not respecting that. Fix that by threading the region through as an explicit parameter and adding an override parameter to the `AwsCliCompatible` base credentials: we don't necessarily want to read this only from the INI files. Additionally: add a validation to make sure that the CDK app respects the region we are requesting. CDK integ tests should either not pass an environment, or read `$CDK_DEFAULT_REGION` to determine the current region. It was too easy to just override the region with a hardcoded one, but that breaks the integ runner's attempts to isolate tests across regions. Guard against that by validating. --- .../@aws-cdk/integ-runner/bin/integ-runner | 3 +- .../integ-runner/lib/engines/toolkit-lib.ts | 51 +++++++++++++++++-- packages/@aws-cdk/integ-runner/lib/run-cli.ts | 2 + .../integ-runner/lib/runner/engine.ts | 7 +-- .../integ-runner/lib/runner/runner-base.ts | 5 ++ .../lib/runner/snapshot-test-runner.ts | 7 ++- .../lib/workers/extract/extract_worker.ts | 4 +- .../test/engines/toolkit-lib.test.ts | 4 +- .../lib/api/aws-auth/base-credentials.ts | 20 +++++++- 9 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 packages/@aws-cdk/integ-runner/lib/run-cli.ts diff --git a/packages/@aws-cdk/integ-runner/bin/integ-runner b/packages/@aws-cdk/integ-runner/bin/integ-runner index 171249b23..258ff0a82 100755 --- a/packages/@aws-cdk/integ-runner/bin/integ-runner +++ b/packages/@aws-cdk/integ-runner/bin/integ-runner @@ -1,4 +1,3 @@ #!/usr/bin/env node -const { cli } = require('../lib'); -cli(); +require('../lib/run-cli'); diff --git a/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts b/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts index 960ca6d38..b0f949ae1 100644 --- a/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts +++ b/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts @@ -1,8 +1,9 @@ import * as path from 'node:path'; import type { DeployOptions, ICdk, ListOptions, SynthFastOptions, SynthOptions, WatchEvents } from '@aws-cdk/cdk-cli-wrapper'; import type { DefaultCdkOptions, DestroyOptions } from '@aws-cdk/cloud-assembly-schema/lib/integ-tests'; -import type { DeploymentMethod, ICloudAssemblySource, IIoHost, IoMessage, IoRequest, NonInteractiveIoHostProps, StackSelector } from '@aws-cdk/toolkit-lib'; -import { ExpandStackSelection, MemoryContext, NonInteractiveIoHost, StackSelectionStrategy, Toolkit } from '@aws-cdk/toolkit-lib'; +import { UNKNOWN_REGION } from '@aws-cdk/cx-api'; +import type { DeploymentMethod, ICloudAssemblySource, IIoHost, IoMessage, IoRequest, IReadableCloudAssembly, NonInteractiveIoHostProps, StackSelector } from '@aws-cdk/toolkit-lib'; +import { BaseCredentials, ExpandStackSelection, MemoryContext, NonInteractiveIoHost, StackSelectionStrategy, Toolkit } from '@aws-cdk/toolkit-lib'; import * as chalk from 'chalk'; import * as fs from 'fs-extra'; @@ -27,6 +28,11 @@ export interface ToolkitLibEngineOptions { * @default false */ readonly showOutput?: boolean; + + /** + * The region the CDK app should synthesize itself for + */ + readonly region: string; } /** @@ -42,7 +48,12 @@ export class ToolkitLibRunnerEngine implements ICdk { this.showOutput = options.showOutput ?? false; this.toolkit = new Toolkit({ - ioHost: this.showOutput? new IntegRunnerIoHost() : new NoopIoHost(), + ioHost: this.showOutput ? new IntegRunnerIoHost() : new NoopIoHost(), + sdkConfig: { + baseCredentials: BaseCredentials.awsCliCompatible({ + defaultRegion: options.region, + }), + }, // @TODO - these options are currently available on the action calls // but toolkit-lib needs them at the constructor level. // Need to decide what to do with them. @@ -73,6 +84,7 @@ export class ToolkitLibRunnerEngine implements ICdk { stacks: this.stackSelector(options), validateStacks: options.validation, }); + await this.validateRegion(lock); await lock.dispose(); } @@ -100,6 +112,7 @@ export class ToolkitLibRunnerEngine implements ICdk { try { // @TODO - use produce to mimic the current behavior more closely const lock = await cx.produce(); + await this.validateRegion(lock); await lock.dispose(); // We should fix this once we have stabilized toolkit-lib as engine. // What we really should do is this: @@ -217,7 +230,6 @@ export class ToolkitLibRunnerEngine implements ICdk { workingDirectory: this.options.workingDirectory, outdir, lookups: options.lookups, - resolveDefaultEnvironment: false, // not part of the integ-runner contract contextStore: new MemoryContext(options.context), env: this.options.env, synthOptions: { @@ -256,6 +268,37 @@ export class ToolkitLibRunnerEngine implements ICdk { method: options.deploymentMethod ?? 'change-set', }; } + + /** + * Check that the regions for the stacks in the CloudAssembly match the regions requested on the engine + * + * This prevents misconfiguration of the integ test app. People tend to put: + * + * ```ts + * new Stack(app, 'Stack', { + * env: { + * region: 'some-region-that-suits-me', + * } + * }); + * ``` + * + * Into their integ tests, instead of: + * + * ```ts + * { + * region: process.env.CDK_DEFAULT_REGION, + * } + * ``` + * + * This catches that misconfiguration. + */ + private async validateRegion(asm: IReadableCloudAssembly) { + for (const stack of asm.cloudAssembly.stacksRecursively) { + if (stack.environment.region !== this.options.region && stack.environment.region !== UNKNOWN_REGION) { + throw new Error(`Stack ${stack.displayName} synthesizes for region ${stack.environment.region}, even though ${this.options.region} was requested. Please configure \`{ env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT } }\`, or use no env at all. Do not hardcode a region or account.`); + } + } + } } /** diff --git a/packages/@aws-cdk/integ-runner/lib/run-cli.ts b/packages/@aws-cdk/integ-runner/lib/run-cli.ts new file mode 100644 index 000000000..d81838b89 --- /dev/null +++ b/packages/@aws-cdk/integ-runner/lib/run-cli.ts @@ -0,0 +1,2 @@ +import { cli } from './cli'; +cli(); diff --git a/packages/@aws-cdk/integ-runner/lib/runner/engine.ts b/packages/@aws-cdk/integ-runner/lib/runner/engine.ts index ee4b19762..a149775ed 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/engine.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/engine.ts @@ -18,9 +18,8 @@ export function makeEngine(options: IntegRunnerOptions): ICdk { return new ToolkitLibRunnerEngine({ workingDirectory: options.test.directory, showOutput: options.showOutput, - env: { - ...options.env, - }, + env: options.env, + region: options.region, }); case 'cli-wrapper': default: @@ -29,6 +28,8 @@ export function makeEngine(options: IntegRunnerOptions): ICdk { showOutput: options.showOutput, env: { ...options.env, + // The CDK CLI will interpret this and use it usefully + AWS_REGION: options.region, }, }); } diff --git a/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts b/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts index 11113104b..26587129c 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/runner-base.ts @@ -26,6 +26,11 @@ export interface IntegRunnerOptions extends EngineOptions { */ readonly test: IntegTest; + /** + * The region where the test should be deployed + */ + readonly region: string; + /** * The AWS profile to use when invoking the CDK CLI * diff --git a/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts b/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts index 21a205fda..766a41876 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/snapshot-test-runner.ts @@ -34,8 +34,11 @@ interface SnapshotAssembly { * the validation of the integration test snapshots */ export class IntegSnapshotRunner extends IntegRunner { - constructor(options: IntegRunnerOptions) { - super(options); + constructor(options: Omit) { + super({ + ...options, + region: 'unused', + }); } /** diff --git a/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts b/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts index 9a87e3809..a124711a4 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts @@ -31,8 +31,8 @@ export async function integTestWorker(request: IntegTestBatchRequest): Promise= 2, @@ -99,8 +99,8 @@ export async function watchTestWorker(options: IntegWatchOptions): Promise engine: options.engine, test, profile: options.profile, + region: options.region, env: { - AWS_REGION: options.region, CDK_DOCKER: process.env.CDK_DOCKER ?? 'docker', }, showOutput: verbosity >= 2, diff --git a/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts b/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts index 2dc440d41..dbfdff076 100644 --- a/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts +++ b/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts @@ -223,9 +223,9 @@ describe('ToolkitLibRunnerEngine', () => { showOutput: true, }); - expect(MockedToolkit).toHaveBeenCalledWith({ + expect(MockedToolkit).toHaveBeenCalledWith(expect.objectContaining({ ioHost: expect.any(Object), - }); + })); }); it('should throw error when no app is provided', async () => { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/base-credentials.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/base-credentials.ts index 182d80c62..293a16add 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/base-credentials.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/base-credentials.ts @@ -92,10 +92,14 @@ export class BaseCredentials { */ public static awsCliCompatible(options: AwsCliCompatibleOptions = {}): IBaseCredentialsProvider { return new class implements IBaseCredentialsProvider { - public sdkBaseConfig(ioHost: IActionAwareIoHost, clientConfig: SdkBaseClientConfig) { + public async sdkBaseConfig(ioHost: IActionAwareIoHost, clientConfig: SdkBaseClientConfig) { const ioHelper = IoHelper.fromActionAwareIoHost(ioHost); const awsCli = new AwsCliCompatible(ioHelper, clientConfig.requestHandler ?? {}, new IoHostSdkLogger(ioHelper)); - return awsCli.baseConfig(options.profile); + + const ret = await awsCli.baseConfig(options.profile); + return options.defaultRegion + ? { ...ret, defaultRegion: options.defaultRegion } + : ret; } public toString() { @@ -140,6 +144,18 @@ export interface AwsCliCompatibleOptions { * @default - Use environment variable if set. */ readonly profile?: string; + + /** + * Use a different default region than the one in the profile + * + * If not supplied the environment variable AWS_REGION will be used, or + * whatever region is set in the indicated profile in `~/.aws/config`. + * If no region is set in the profile the region in `[default]` will + * be used. + * + * @default - Use region from `~/.aws/config`. + */ + readonly defaultRegion?: string; } export interface CustomBaseCredentialsOption { From ac111eb24a058bb89d8c611b92488b286d25f227 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 19 Nov 2025 11:11:55 +0100 Subject: [PATCH 2/4] Update mocks --- .../test/engines/toolkit-lib-snapshot-path.test.ts | 7 ++++--- .../@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib-snapshot-path.test.ts b/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib-snapshot-path.test.ts index af2fb8336..d20d07d18 100644 --- a/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib-snapshot-path.test.ts +++ b/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib-snapshot-path.test.ts @@ -25,6 +25,7 @@ describe('ToolkitLibRunnerEngine - Snapshot Path Handling', () => { engine = new ToolkitLibRunnerEngine({ workingDirectory: '/test/dir', + region: 'us-dummy-1', }); }); @@ -32,7 +33,7 @@ describe('ToolkitLibRunnerEngine - Snapshot Path Handling', () => { const snapshotPath = 'test.snapshot'; const fullSnapshotPath = path.join('/test/dir', snapshotPath); const mockCx = { produce: jest.fn() }; - const mockLock = { dispose: jest.fn() }; + const mockLock = { dispose: jest.fn(), cloudAssembly: { stacksRecursively: [] } }; // Mock fs to indicate the snapshot directory exists mockedFs.pathExistsSync.mockReturnValue(true); @@ -55,7 +56,7 @@ describe('ToolkitLibRunnerEngine - Snapshot Path Handling', () => { it('should use fromCdkApp when app is not a path to existing directory', async () => { const appCommand = 'node bin/app.js'; const mockCx = { produce: jest.fn() }; - const mockLock = { dispose: jest.fn() }; + const mockLock = { dispose: jest.fn(), cloudAssembly: { stacksRecursively: [] } }; // Mock fs to indicate the path doesn't exist mockedFs.pathExistsSync.mockReturnValue(false); @@ -76,7 +77,7 @@ describe('ToolkitLibRunnerEngine - Snapshot Path Handling', () => { const appPath = 'app.js'; const fullAppPath = path.join('/test/dir', appPath); const mockCx = { produce: jest.fn() }; - const mockLock = { dispose: jest.fn() }; + const mockLock = { dispose: jest.fn(), cloudAssembly: { stacksRecursively: [] } }; // Mock fs to indicate the path exists but is not a directory mockedFs.pathExistsSync.mockReturnValue(true); diff --git a/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts b/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts index dbfdff076..a3a398330 100644 --- a/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts +++ b/packages/@aws-cdk/integ-runner/test/engines/toolkit-lib.test.ts @@ -25,13 +25,14 @@ describe('ToolkitLibRunnerEngine', () => { engine = new ToolkitLibRunnerEngine({ workingDirectory: '/test/dir', + region: 'us-dummy-1', }); }); describe('synth', () => { it('should call toolkit.synth with correct parameters', async () => { const mockCx = { produce: jest.fn() }; - const mockLock = { dispose: jest.fn() }; + const mockLock = { dispose: jest.fn(), cloudAssembly: { stacksRecursively: [] } }; mockToolkit.fromCdkApp.mockResolvedValue(mockCx as any); mockToolkit.synth.mockResolvedValue(mockLock as any); @@ -56,7 +57,7 @@ describe('ToolkitLibRunnerEngine', () => { describe('synthFast', () => { it('should use fromCdkApp and produce for fast synthesis', async () => { const mockCx = { produce: jest.fn() }; - const mockLock = { dispose: jest.fn() }; + const mockLock = { dispose: jest.fn(), cloudAssembly: { stacksRecursively: [] } }; mockCx.produce.mockResolvedValue(mockLock); mockToolkit.fromCdkApp.mockResolvedValue(mockCx as any); @@ -221,6 +222,7 @@ describe('ToolkitLibRunnerEngine', () => { const engineWithOutput = new ToolkitLibRunnerEngine({ workingDirectory: '/test', showOutput: true, + region: 'us-dummy-1', }); expect(MockedToolkit).toHaveBeenCalledWith(expect.objectContaining({ From 7a4a4970b78b462c423000679c76b6e80c257884 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 19 Nov 2025 11:35:04 +0100 Subject: [PATCH 3/4] Undo run-cli.ts changes --- packages/@aws-cdk/integ-runner/bin/integ-runner | 3 ++- packages/@aws-cdk/integ-runner/lib/run-cli.ts | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 packages/@aws-cdk/integ-runner/lib/run-cli.ts diff --git a/packages/@aws-cdk/integ-runner/bin/integ-runner b/packages/@aws-cdk/integ-runner/bin/integ-runner index 258ff0a82..171249b23 100755 --- a/packages/@aws-cdk/integ-runner/bin/integ-runner +++ b/packages/@aws-cdk/integ-runner/bin/integ-runner @@ -1,3 +1,4 @@ #!/usr/bin/env node -require('../lib/run-cli'); +const { cli } = require('../lib'); +cli(); diff --git a/packages/@aws-cdk/integ-runner/lib/run-cli.ts b/packages/@aws-cdk/integ-runner/lib/run-cli.ts deleted file mode 100644 index d81838b89..000000000 --- a/packages/@aws-cdk/integ-runner/lib/run-cli.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { cli } from './cli'; -cli(); From 207a22e0e83d77e1401377c3c27cf9fcdd3969ff Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 19 Nov 2025 11:48:59 +0100 Subject: [PATCH 4/4] Turn error into a warning --- .../integ-runner/lib/engines/toolkit-lib.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts b/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts index b0f949ae1..e6f16e362 100644 --- a/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts +++ b/packages/@aws-cdk/integ-runner/lib/engines/toolkit-lib.ts @@ -42,13 +42,18 @@ export class ToolkitLibRunnerEngine implements ICdk { private readonly toolkit: Toolkit; private readonly options: ToolkitLibEngineOptions; private readonly showOutput: boolean; + private readonly ioHost: IntegRunnerIoHost; public constructor(options: ToolkitLibEngineOptions) { this.options = options; this.showOutput = options.showOutput ?? false; + // We always create this for ourselves to emit warnings, but potentially + // don't pass it to the toolkit. + this.ioHost = new IntegRunnerIoHost(); + this.toolkit = new Toolkit({ - ioHost: this.showOutput ? new IntegRunnerIoHost() : new NoopIoHost(), + ioHost: this.showOutput ? this.ioHost : new NoopIoHost(), sdkConfig: { baseCredentials: BaseCredentials.awsCliCompatible({ defaultRegion: options.region, @@ -295,7 +300,23 @@ export class ToolkitLibRunnerEngine implements ICdk { private async validateRegion(asm: IReadableCloudAssembly) { for (const stack of asm.cloudAssembly.stacksRecursively) { if (stack.environment.region !== this.options.region && stack.environment.region !== UNKNOWN_REGION) { - throw new Error(`Stack ${stack.displayName} synthesizes for region ${stack.environment.region}, even though ${this.options.region} was requested. Please configure \`{ env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT } }\`, or use no env at all. Do not hardcode a region or account.`); + this.ioHost.notify({ + action: 'deploy', + code: 'CDK_RUNNER_W0000', + time: new Date(), + level: 'warn', + message: `Stack ${stack.displayName} synthesizes for region ${stack.environment.region}, even though ${this.options.region} was requested. Please configure \`{ env: { region: process.env.CDK_DEFAULT_REGION, account: process.env.CDK_DEFAULT_ACCOUNT } }\`, or use no env at all. Do not hardcode a region or account.`, + data: { + stackName: stack.displayName, + stackRegion: stack.environment.region, + requestedRegion: this.options.region, + }, + }).catch((e) => { + if (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }); } } } @@ -312,9 +333,16 @@ class IntegRunnerIoHost extends NonInteractiveIoHost { }); } public async notify(msg: IoMessage): Promise { + let color; + switch (msg.level) { + case 'error': color = chalk.red; break; + case 'warn': color = chalk.yellow; break; + default: color = chalk.gray; + } + return super.notify({ ...msg, - message: chalk.gray(msg.message), + message: color(msg.message), }); } }