Skip to content

Commit 6014d9b

Browse files
authored
Merge branch 'main' into bdoyle0182/fix-managed-compute-environment-token-skip-validation
2 parents 565b1de + 6fa1d05 commit 6014d9b

File tree

7 files changed

+150
-170
lines changed

7 files changed

+150
-170
lines changed

packages/aws-cdk/lib/cli/cdk-toolkit.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ import {
4747
import { printSecurityDiff, printStackDiff, RequireApproval } from '../diff';
4848
import { ResourceImporter, removeNonImportResources } from '../import';
4949
import { listStacks } from '../list-stacks';
50-
import { data, debug, error, highlight, info, success, warning, withCorkedLogging } from '../logging';
50+
import { data, debug, error, highlight, info, success, warning } from '../logging';
5151
import { ResourceMigrator } from '../migrator';
5252
import { deserializeStructure, obscureTemplate, serializeStructure } from '../serialize';
53+
import { CliIoHost } from '../toolkit/cli-io-host';
5354
import { ToolkitError } from '../toolkit/error';
5455
import { numberFromBool, partition } from '../util';
5556
import { formatErrorMessage } from '../util/error';
@@ -79,6 +80,11 @@ export interface CdkToolkitProps {
7980
*/
8081
deployments: Deployments;
8182

83+
/**
84+
* The CliIoHost that's used for I/O operations
85+
*/
86+
ioHost?: CliIoHost;
87+
8288
/**
8389
* Whether to be verbose
8490
*
@@ -136,7 +142,11 @@ export enum AssetBuildTime {
136142
* deploys applies them to `cloudFormation`.
137143
*/
138144
export class CdkToolkit {
139-
constructor(private readonly props: CdkToolkitProps) {}
145+
private ioHost: CliIoHost;
146+
147+
constructor(private readonly props: CdkToolkitProps) {
148+
this.ioHost = props.ioHost ?? CliIoHost.instance();
149+
}
140150

141151
public async metadata(stackName: string, json: boolean) {
142152
const stacks = await this.selectSingleStackByName(stackName);
@@ -371,6 +381,7 @@ export class CdkToolkit {
371381
const currentTemplate = await this.props.deployments.readCurrentTemplate(stack);
372382
if (printSecurityDiff(currentTemplate, stack, requireApproval)) {
373383
await askUserConfirmation(
384+
this.ioHost,
374385
concurrency,
375386
'"--require-approval" is enabled and stack includes security-sensitive updates',
376387
'Do you wish to deploy these changes',
@@ -451,6 +462,7 @@ export class CdkToolkit {
451462
warning(`${motivation}. Rolling back first (--force).`);
452463
} else {
453464
await askUserConfirmation(
465+
this.ioHost,
454466
concurrency,
455467
motivation,
456468
`${motivation}. Roll back first and then proceed with deployment`,
@@ -476,6 +488,7 @@ export class CdkToolkit {
476488
warning(`${motivation}. Proceeding with regular deployment (--force).`);
477489
} else {
478490
await askUserConfirmation(
491+
this.ioHost,
479492
concurrency,
480493
motivation,
481494
`${motivation}. Perform a regular deployment`,
@@ -1818,11 +1831,12 @@ function buildParameterMap(
18181831
* cannot be interactively obtained from a human at the keyboard.
18191832
*/
18201833
async function askUserConfirmation(
1834+
ioHost: CliIoHost,
18211835
concurrency: number,
18221836
motivation: string,
18231837
question: string,
18241838
) {
1825-
await withCorkedLogging(async () => {
1839+
await ioHost.withCorkedLogging(async () => {
18261840
// only talk to user if STDIN is a terminal (otherwise, fail)
18271841
if (!TESTING && !process.stdin.isTTY) {
18281842
throw new ToolkitError(`${motivation}, but terminal (TTY) is not attached so we are unable to get a confirmation from the user`);

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

+1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
195195
};
196196

197197
const cli = new CdkToolkit({
198+
ioHost,
198199
cloudExecutable,
199200
deployments: cloudFormation,
200201
verbose: argv.trace || argv.verbose > 0,

packages/aws-cdk/lib/logging.ts

+9-77
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,11 @@
11
import * as util from 'util';
22
import * as chalk from 'chalk';
3-
import { IoMessageLevel, IoMessage, CliIoHost, IoMessageSpecificCode, IoMessageCode, IoMessageCodeCategory, IoCodeLevel, levelPriority } from './toolkit/cli-io-host';
4-
5-
// Corking mechanism
6-
let CORK_COUNTER = 0;
7-
const logBuffer: IoMessage<any>[] = [];
8-
9-
/**
10-
* Executes a block of code with corked logging. All log messages during execution
11-
* are buffered and only written when all nested cork blocks complete (when CORK_COUNTER reaches 0).
12-
* @param block - Async function to execute with corked logging
13-
* @returns Promise that resolves with the block's return value
14-
*/
15-
export async function withCorkedLogging<T>(block: () => Promise<T>): Promise<T> {
16-
CORK_COUNTER++;
17-
try {
18-
return await block();
19-
} finally {
20-
CORK_COUNTER--;
21-
if (CORK_COUNTER === 0) {
22-
// Process each buffered message through notify
23-
for (const ioMessage of logBuffer) {
24-
void CliIoHost.instance().notify(ioMessage);
25-
}
26-
logBuffer.splice(0);
27-
}
28-
}
29-
}
30-
31-
interface LogMessage {
32-
/**
33-
* The log level to use
34-
*/
35-
readonly level: IoMessageLevel;
36-
/**
37-
* The message to log
38-
*/
39-
readonly message: string;
40-
/**
41-
* Whether to force stdout
42-
* @default false
43-
*/
44-
readonly forceStdout?: boolean;
45-
/**
46-
* Message code of the format [CATEGORY]_[NUMBER_CODE]
47-
* @pattern [A-Z]+_[0-2][0-9]{3}
48-
* @default TOOLKIT_[0/1/2]000
49-
*/
50-
readonly code: IoMessageCode;
51-
}
52-
53-
/**
54-
* Internal core logging function that writes messages through the CLI IO host.
55-
* @param msg Configuration options for the log message. See {@link LogMessage}
56-
*/
57-
function log(msg: LogMessage) {
58-
const ioMessage: IoMessage<undefined> = {
59-
level: msg.level,
60-
message: msg.message,
61-
forceStdout: msg.forceStdout,
62-
time: new Date(),
63-
action: CliIoHost.instance().currentAction,
64-
code: msg.code,
65-
};
66-
67-
if (CORK_COUNTER > 0) {
68-
if (levelPriority[msg.level] > levelPriority[CliIoHost.instance().logLevel]) {
69-
return;
70-
}
71-
logBuffer.push(ioMessage);
72-
return;
73-
}
74-
75-
void CliIoHost.instance().notify(ioMessage);
76-
}
3+
import { IoMessageLevel, IoMessage, CliIoHost, IoMessageSpecificCode, IoMessageCode, IoMessageCodeCategory, IoCodeLevel } from './toolkit/cli-io-host';
774

785
/**
796
* Internal helper that processes log inputs into a consistent format.
807
* Handles string interpolation, format strings, and object parameter styles.
81-
* Applies optional styling and prepares the final message for logging.
8+
* Applies optional styling and sends the message to the IoHost.
829
*/
8310
function formatMessageAndLog(
8411
level: IoMessageLevel,
@@ -98,12 +25,17 @@ function formatMessageAndLog(
9825
// Apply style if provided
9926
const finalMessage = style ? style(formattedMessage) : formattedMessage;
10027

101-
log({
28+
const ioHost = CliIoHost.instance();
29+
const ioMessage: IoMessage<undefined> = {
30+
time: new Date(),
31+
action: ioHost.currentAction,
10232
level,
10333
message: finalMessage,
10434
code,
10535
forceStdout,
106-
});
36+
};
37+
38+
void ioHost.notify(ioMessage);
10739
}
10840

10941
function getDefaultCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'TOOLKIT'): IoMessageCode {

packages/aws-cdk/lib/toolkit/cli-io-host.ts

+38-12
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,17 @@ export class CliIoHost implements IIoHost {
175175
*/
176176
private static _instance: CliIoHost | undefined;
177177

178+
// internal state for getters/setter
178179
private _currentAction: ToolkitAction;
179180
private _isCI: boolean;
180181
private _isTTY: boolean;
181182
private _logLevel: IoMessageLevel;
182183
private _internalIoHost?: IIoHost;
183184

185+
// Corked Logging
186+
private corkedCounter = 0;
187+
private readonly corkedLoggingBuffer: IoMessage<any>[] = [];
188+
184189
private constructor(props: CliIoHostProps = {}) {
185190
this._currentAction = props.currentAction ?? 'none' as ToolkitAction;
186191
this._isTTY = props.isTTY ?? process.stdout.isTTY ?? false;
@@ -259,6 +264,31 @@ export class CliIoHost implements IIoHost {
259264
this._logLevel = level;
260265
}
261266

267+
/**
268+
* Executes a block of code with corked logging. All log messages during execution
269+
* are buffered and only written when all nested cork blocks complete (when CORK_COUNTER reaches 0).
270+
* The corking is bound to the specific instance of the CliIoHost.
271+
*
272+
* @param block - Async function to execute with corked logging
273+
* @returns Promise that resolves with the block's return value
274+
*/
275+
public async withCorkedLogging<T>(block: () => Promise<T>): Promise<T> {
276+
this.corkedCounter++;
277+
try {
278+
return await block();
279+
} finally {
280+
this.corkedCounter--;
281+
if (this.corkedCounter === 0) {
282+
// Process each buffered message through notify
283+
for (const ioMessage of this.corkedLoggingBuffer) {
284+
await this.notify(ioMessage);
285+
}
286+
// remove all buffered messages in-place
287+
this.corkedLoggingBuffer.splice(0);
288+
}
289+
}
290+
}
291+
262292
/**
263293
* Notifies the host of a message.
264294
* The caller waits until the notification completes.
@@ -272,24 +302,20 @@ export class CliIoHost implements IIoHost {
272302
return;
273303
}
274304

305+
if (this.corkedCounter > 0) {
306+
this.corkedLoggingBuffer.push(msg);
307+
return;
308+
}
309+
275310
const output = this.formatMessage(msg);
276-
const stream = this.stream(msg.level, msg.forceStdout ?? false);
277-
278-
return new Promise((resolve, reject) => {
279-
stream.write(output, (err) => {
280-
if (err) {
281-
reject(err);
282-
} else {
283-
resolve();
284-
}
285-
});
286-
});
311+
const stream = this.selectStream(msg.level, msg.forceStdout ?? false);
312+
stream.write(output);
287313
}
288314

289315
/**
290316
* Determines which output stream to use based on log level and configuration.
291317
*/
292-
private stream(level: IoMessageLevel, forceStdout: boolean) {
318+
private selectStream(level: IoMessageLevel, forceStdout: boolean) {
293319
// For legacy purposes all log streams are written to stderr by default, unless
294320
// specified otherwise, by passing `forceStdout`, which is used by the `data()` logging function, or
295321
// if the CDK is running in a CI environment. This is because some CI environments will immediately

packages/aws-cdk/test/api/logs/logging.test.ts

+1-60
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { data, success, highlight, error, warning, info, debug, trace, withCorkedLogging } from '../../../lib/logging';
1+
import { data, success, highlight, error, warning, info, debug, trace } from '../../../lib/logging';
22
import { CliIoHost } from '../../../lib/toolkit/cli-io-host';
33

44
describe('logging', () => {
@@ -249,65 +249,6 @@ describe('logging', () => {
249249
});
250250
});
251251

252-
describe('corked logging', () => {
253-
test('buffers messages when corked', async () => {
254-
await withCorkedLogging(async () => {
255-
info('message 1');
256-
info({ message: 'message 2' });
257-
expect(mockStderr).not.toHaveBeenCalled();
258-
});
259-
260-
expect(mockStderr).toHaveBeenCalledWith('message 1\n');
261-
expect(mockStderr).toHaveBeenCalledWith('message 2\n');
262-
});
263-
264-
test('handles nested corking correctly', async () => {
265-
await withCorkedLogging(async () => {
266-
info('outer 1');
267-
await withCorkedLogging(async () => {
268-
info({ message: 'inner' });
269-
});
270-
info({ message: 'outer 2' });
271-
expect(mockStderr).not.toHaveBeenCalled();
272-
});
273-
274-
expect(mockStderr).toHaveBeenCalledTimes(3);
275-
expect(mockStderr).toHaveBeenCalledWith('outer 1\n');
276-
expect(mockStderr).toHaveBeenCalledWith('inner\n');
277-
expect(mockStderr).toHaveBeenCalledWith('outer 2\n');
278-
});
279-
280-
test('handles errors in corked block while preserving buffer', async () => {
281-
await expect(withCorkedLogging(async () => {
282-
info('message 1');
283-
throw new Error('test error');
284-
})).rejects.toThrow('test error');
285-
286-
// The buffered message should still be printed even if the block throws
287-
expect(mockStderr).toHaveBeenCalledWith('message 1\n');
288-
});
289-
290-
test('maintains correct order with mixed log levels in corked block', async () => {
291-
// Set threshold to debug to allow debug messages
292-
ioHost.logLevel = 'debug';
293-
294-
await withCorkedLogging(async () => {
295-
error('error message');
296-
warning('warning message');
297-
success('success message');
298-
debug('debug message');
299-
});
300-
301-
const calls = mockStderr.mock.calls.map(call => call[0]);
302-
expect(calls).toEqual([
303-
'error message\n',
304-
'warning message\n',
305-
'success message\n',
306-
expect.stringMatching(/^\[\d{2}:\d{2}:\d{2}\] debug message\n$/),
307-
]);
308-
});
309-
});
310-
311252
describe('CI mode behavior', () => {
312253
test('correctly switches between stdout and stderr based on CI mode', () => {
313254
ioHost.isCI = true;

0 commit comments

Comments
 (0)