Skip to content

Commit 0643020

Browse files
authoredMay 19, 2023
feat(cli): logging can be corked (#25644)
🍾 This PR extends #25536 by fixing issues with logging. - Asset building and publishing are now completely separate tasks, so there is never a need for the "Building and Publishing" message in cdk-assets. I've removed a good chunk of unnecessary private props in `AssetPublisher` and now we simply print `Building` when building an asset and `Publishing` when publishing an asset. No combos anymore. - Asset build/publish can now happen concurrently with stack deployments when there are no dependencies between the two, but if `--require-approval` is set (which it is by default), sensitive stack deployments prompt the user for a `y/n` response before deployment. Additional asset related messages may come in at this time, cluttering the log. The solution here is to implement a cork that is turned on when prompting the user and turned off after user input. When using the helper function `withCorkedLogging(callback)`, logs will instead be stored in memory and released when the cork is popped. Testing: There's not a great way to test these changes in code since they should only affect logging. Instead, I hope the following photos suffice: Before the lock change, logging looked like this: <img width="698" alt="Screen Shot 2023-05-18 at 4 59 35 PM" src="https://github.com/aws/aws-cdk/assets/36202692/c554c1f2-1034-422c-95e6-ebf15f09b35b"> Now it looks like this in the same scenario: <img width="697" alt="Screen Shot 2023-05-18 at 4 49 39 PM" src="https://github.com/aws/aws-cdk/assets/36202692/1257c20e-73c4-4fc2-b8cd-ae510f32c756"> The screenshots also show the logs that say `Building` and `Publishing` separately rather than `Building and Publishing` as it did before. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b60876f commit 0643020

File tree

4 files changed

+66
-57
lines changed

4 files changed

+66
-57
lines changed
 

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

+19-18
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
1717
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
1818
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
1919
import { ResourceImporter } from './import';
20-
import { data, debug, error, highlight, print, success, warning } from './logging';
20+
import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging';
2121
import { deserializeStructure, serializeStructure } from './serialize';
2222
import { Configuration, PROJECT_CONFIG } from './settings';
2323
import { numberFromBool, partition } from './util';
@@ -238,23 +238,24 @@ export class CdkToolkit {
238238
if (requireApproval !== RequireApproval.Never) {
239239
const currentTemplate = await this.props.deployments.readCurrentTemplate(stack);
240240
if (printSecurityDiff(currentTemplate, stack, requireApproval)) {
241-
242-
// only talk to user if STDIN is a terminal (otherwise, fail)
243-
if (!process.stdin.isTTY) {
244-
throw new Error(
245-
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
246-
'but terminal (TTY) is not attached so we are unable to get a confirmation from the user');
247-
}
248-
249-
// only talk to user if concurrency is 1 (otherwise, fail)
250-
if (concurrency > 1) {
251-
throw new Error(
252-
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
253-
'but concurrency is greater than 1 so we are unable to get a confirmation from the user');
254-
}
255-
256-
const confirmed = await promptly.confirm('Do you wish to deploy these changes (y/n)?');
257-
if (!confirmed) { throw new Error('Aborted by user'); }
241+
await withCorkedLogging(async () => {
242+
// only talk to user if STDIN is a terminal (otherwise, fail)
243+
if (!process.stdin.isTTY) {
244+
throw new Error(
245+
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
246+
'but terminal (TTY) is not attached so we are unable to get a confirmation from the user');
247+
}
248+
249+
// only talk to user if concurrency is 1 (otherwise, fail)
250+
if (concurrency > 1) {
251+
throw new Error(
252+
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
253+
'but concurrency is greater than 1 so we are unable to get a confirmation from the user');
254+
}
255+
256+
const confirmed = await promptly.confirm('Do you wish to deploy these changes (y/n)?');
257+
if (!confirmed) { throw new Error('Aborted by user'); }
258+
});
258259
}
259260
}
260261

‎packages/aws-cdk/lib/logging.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ const { stdout, stderr } = process;
77

88
type WritableFactory = () => Writable;
99

10+
export async function withCorkedLogging<A>(block: () => Promise<A>): Promise<A> {
11+
corkLogging();
12+
try {
13+
return await block();
14+
} finally {
15+
uncorkLogging();
16+
}
17+
}
18+
19+
let CORK_COUNTER = 0;
20+
const logBuffer: [Writable, string][] = [];
21+
22+
function corked() {
23+
return CORK_COUNTER !== 0;
24+
}
25+
26+
function corkLogging() {
27+
CORK_COUNTER += 1;
28+
}
29+
30+
function uncorkLogging() {
31+
CORK_COUNTER -= 1;
32+
if (!corked()) {
33+
logBuffer.forEach(([stream, str]) => stream.write(str + '\n'));
34+
logBuffer.splice(0);
35+
}
36+
}
37+
1038
const logger = (stream: Writable | WritableFactory, styles?: StyleFn[], timestamp?: boolean) => (fmt: string, ...args: unknown[]) => {
1139
const ts = timestamp ? `[${formatTime(new Date())}] ` : '';
1240

@@ -15,11 +43,19 @@ const logger = (stream: Writable | WritableFactory, styles?: StyleFn[], timestam
1543
str = styles.reduce((a, style) => style(a), str);
1644
}
1745

18-
1946
const realStream = typeof stream === 'function' ? stream() : stream;
47+
48+
// Logger is currently corked, so we store the message to be printed
49+
// later when we are uncorked.
50+
if (corked()) {
51+
logBuffer.push([realStream, str]);
52+
return;
53+
}
54+
2055
realStream.write(str + '\n');
2156
};
2257

58+
2359
function formatTime(d: Date) {
2460
return `${lpad(d.getHours(), 2)}:${lpad(d.getMinutes(), 2)}:${lpad(d.getSeconds(), 2)}`;
2561

‎packages/cdk-assets/lib/publishing.ts

+9-37
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ export class AssetPublishing implements IPublishProgress {
8282
private readonly publishInParallel: boolean;
8383
private readonly buildAssets: boolean;
8484
private readonly publishAssets: boolean;
85-
private readonly startMessagePrefix: string;
86-
private readonly successMessagePrefix: string;
87-
private readonly errorMessagePrefix: string;
8885
private readonly handlerCache = new Map<IManifestEntry, IAssetHandler>();
8986

9087
constructor(private readonly manifest: AssetManifest, private readonly options: AssetPublishingOptions) {
@@ -94,34 +91,6 @@ export class AssetPublishing implements IPublishProgress {
9491
this.buildAssets = options.buildAssets ?? true;
9592
this.publishAssets = options.publishAssets ?? true;
9693

97-
const getMessages = () => {
98-
if (this.buildAssets && this.publishAssets) {
99-
return {
100-
startMessagePrefix: 'Building and publishing',
101-
successMessagePrefix: 'Built and published',
102-
errorMessagePrefix: 'Error building and publishing',
103-
};
104-
} else if (this.buildAssets) {
105-
return {
106-
startMessagePrefix: 'Building',
107-
successMessagePrefix: 'Built',
108-
errorMessagePrefix: 'Error building',
109-
};
110-
} else {
111-
return {
112-
startMessagePrefix: 'Publishing',
113-
successMessagePrefix: 'Published',
114-
errorMessagePrefix: 'Error publishing',
115-
};
116-
}
117-
};
118-
119-
const messages = getMessages();
120-
121-
this.startMessagePrefix = messages.startMessagePrefix;
122-
this.successMessagePrefix = messages.successMessagePrefix;
123-
this.errorMessagePrefix = messages.errorMessagePrefix;
124-
12594
const self = this;
12695
this.handlerHost = {
12796
aws: this.options.aws,
@@ -146,7 +115,7 @@ export class AssetPublishing implements IPublishProgress {
146115
}
147116

148117
if ((this.options.throwOnError ?? true) && this.failures.length > 0) {
149-
throw new Error(`${this.errorMessagePrefix}: ${this.failures.map(e => e.error.message)}`);
118+
throw new Error(`Error publishing: ${this.failures.map(e => e.error.message)}`);
150119
}
151120
}
152121

@@ -155,14 +124,17 @@ export class AssetPublishing implements IPublishProgress {
155124
*/
156125
public async buildEntry(asset: IManifestEntry) {
157126
try {
158-
if (this.progressEvent(EventType.START, `${this.startMessagePrefix} ${asset.id}`)) { return false; }
127+
if (this.progressEvent(EventType.START, `Building ${asset.id}`)) { return false; }
159128

160129
const handler = this.assetHandler(asset);
161130
await handler.build();
162131

163132
if (this.aborted) {
164133
throw new Error('Aborted');
165134
}
135+
136+
this.completedOperations++;
137+
if (this.progressEvent(EventType.SUCCESS, `Built ${asset.id}`)) { return false; }
166138
} catch (e: any) {
167139
this.failures.push({ asset, error: e });
168140
this.completedOperations++;
@@ -177,7 +149,7 @@ export class AssetPublishing implements IPublishProgress {
177149
*/
178150
public async publishEntry(asset: IManifestEntry) {
179151
try {
180-
if (this.progressEvent(EventType.UPLOAD, `${this.startMessagePrefix} ${asset.id}`)) { return false; }
152+
if (this.progressEvent(EventType.START, `Publishing ${asset.id}`)) { return false; }
181153

182154
const handler = this.assetHandler(asset);
183155
await handler.publish();
@@ -187,7 +159,7 @@ export class AssetPublishing implements IPublishProgress {
187159
}
188160

189161
this.completedOperations++;
190-
if (this.progressEvent(EventType.SUCCESS, `${this.successMessagePrefix} ${asset.id}`)) { return false; }
162+
if (this.progressEvent(EventType.SUCCESS, `Published ${asset.id}`)) { return false; }
191163
} catch (e: any) {
192164
this.failures.push({ asset, error: e });
193165
this.completedOperations++;
@@ -212,7 +184,7 @@ export class AssetPublishing implements IPublishProgress {
212184
*/
213185
private async publishAsset(asset: IManifestEntry) {
214186
try {
215-
if (this.progressEvent(EventType.START, `${this.startMessagePrefix} ${asset.id}`)) { return false; }
187+
if (this.progressEvent(EventType.START, `Publishing ${asset.id}`)) { return false; }
216188

217189
const handler = this.assetHandler(asset);
218190

@@ -229,7 +201,7 @@ export class AssetPublishing implements IPublishProgress {
229201
}
230202

231203
this.completedOperations++;
232-
if (this.progressEvent(EventType.SUCCESS, `${this.successMessagePrefix} ${asset.id}`)) { return false; }
204+
if (this.progressEvent(EventType.SUCCESS, `Published ${asset.id}`)) { return false; }
233205
} catch (e: any) {
234206
this.failures.push({ asset, error: e });
235207
this.completedOperations++;

‎packages/cdk-assets/test/docker-images.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ describe('with a complete manifest', () => {
247247
test('Displays an error if the ECR repository cannot be found', async () => {
248248
aws.mockEcr.describeImages = mockedApiFailure('RepositoryNotFoundException', 'Repository not Found');
249249

250-
await expect(pub.publish()).rejects.toThrow('Error building and publishing: Repository not Found');
250+
await expect(pub.publish()).rejects.toThrow('Error publishing: Repository not Found');
251251
});
252252

253253
test('successful run does not need to query account ID', async () => {

0 commit comments

Comments
 (0)
Please sign in to comment.