Skip to content

feat(cli): logging can be corked #25644

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

Merged
merged 7 commits into from
May 19, 2023
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
37 changes: 19 additions & 18 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
import { ResourceImporter } from './import';
import { data, debug, error, highlight, print, success, warning } from './logging';
import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging';
import { deserializeStructure, serializeStructure } from './serialize';
import { Configuration, PROJECT_CONFIG } from './settings';
import { numberFromBool, partition } from './util';
Expand Down Expand Up @@ -238,23 +238,24 @@ export class CdkToolkit {
if (requireApproval !== RequireApproval.Never) {
const currentTemplate = await this.props.deployments.readCurrentTemplate(stack);
if (printSecurityDiff(currentTemplate, stack, requireApproval)) {

// only talk to user if STDIN is a terminal (otherwise, fail)
if (!process.stdin.isTTY) {
throw new Error(
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
'but terminal (TTY) is not attached so we are unable to get a confirmation from the user');
}

// only talk to user if concurrency is 1 (otherwise, fail)
if (concurrency > 1) {
throw new Error(
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
'but concurrency is greater than 1 so we are unable to get a confirmation from the user');
}

const confirmed = await promptly.confirm('Do you wish to deploy these changes (y/n)?');
if (!confirmed) { throw new Error('Aborted by user'); }
await withCorkedLogging(async () => {
// only talk to user if STDIN is a terminal (otherwise, fail)
if (!process.stdin.isTTY) {
throw new Error(
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
'but terminal (TTY) is not attached so we are unable to get a confirmation from the user');
}

// only talk to user if concurrency is 1 (otherwise, fail)
if (concurrency > 1) {
throw new Error(
'"--require-approval" is enabled and stack includes security-sensitive updates, ' +
'but concurrency is greater than 1 so we are unable to get a confirmation from the user');
}

const confirmed = await promptly.confirm('Do you wish to deploy these changes (y/n)?');
if (!confirmed) { throw new Error('Aborted by user'); }
});
}
}

Expand Down
38 changes: 37 additions & 1 deletion packages/aws-cdk/lib/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ const { stdout, stderr } = process;

type WritableFactory = () => Writable;

export async function withCorkedLogging<A>(block: () => Promise<A>): Promise<A> {
corkLogging();
try {
return await block();
} finally {
uncorkLogging();
}
}

let CORK_COUNTER = 0;
const logBuffer: [Writable, string][] = [];

function corked() {
return CORK_COUNTER !== 0;
}

function corkLogging() {
CORK_COUNTER += 1;
}

function uncorkLogging() {
CORK_COUNTER -= 1;
if (!corked()) {
logBuffer.forEach(([stream, str]) => stream.write(str + '\n'));
logBuffer.splice(0);
}
}

const logger = (stream: Writable | WritableFactory, styles?: StyleFn[], timestamp?: boolean) => (fmt: string, ...args: unknown[]) => {
const ts = timestamp ? `[${formatTime(new Date())}] ` : '';

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


const realStream = typeof stream === 'function' ? stream() : stream;

// Logger is currently corked, so we store the message to be printed
// later when we are uncorked.
if (corked()) {
logBuffer.push([realStream, str]);
return;
}

realStream.write(str + '\n');
};


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

Expand Down
46 changes: 9 additions & 37 deletions packages/cdk-assets/lib/publishing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,6 @@ export class AssetPublishing implements IPublishProgress {
private readonly publishInParallel: boolean;
private readonly buildAssets: boolean;
private readonly publishAssets: boolean;
private readonly startMessagePrefix: string;
private readonly successMessagePrefix: string;
private readonly errorMessagePrefix: string;
private readonly handlerCache = new Map<IManifestEntry, IAssetHandler>();

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

const getMessages = () => {
if (this.buildAssets && this.publishAssets) {
return {
startMessagePrefix: 'Building and publishing',
successMessagePrefix: 'Built and published',
errorMessagePrefix: 'Error building and publishing',
};
} else if (this.buildAssets) {
return {
startMessagePrefix: 'Building',
successMessagePrefix: 'Built',
errorMessagePrefix: 'Error building',
};
} else {
return {
startMessagePrefix: 'Publishing',
successMessagePrefix: 'Published',
errorMessagePrefix: 'Error publishing',
};
}
};

const messages = getMessages();

this.startMessagePrefix = messages.startMessagePrefix;
this.successMessagePrefix = messages.successMessagePrefix;
this.errorMessagePrefix = messages.errorMessagePrefix;

const self = this;
this.handlerHost = {
aws: this.options.aws,
Expand All @@ -146,7 +115,7 @@ export class AssetPublishing implements IPublishProgress {
}

if ((this.options.throwOnError ?? true) && this.failures.length > 0) {
throw new Error(`${this.errorMessagePrefix}: ${this.failures.map(e => e.error.message)}`);
throw new Error(`Error publishing: ${this.failures.map(e => e.error.message)}`);
}
}

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

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

if (this.aborted) {
throw new Error('Aborted');
}

this.completedOperations++;
if (this.progressEvent(EventType.SUCCESS, `Built ${asset.id}`)) { return false; }
} catch (e: any) {
this.failures.push({ asset, error: e });
this.completedOperations++;
Expand All @@ -177,7 +149,7 @@ export class AssetPublishing implements IPublishProgress {
*/
public async publishEntry(asset: IManifestEntry) {
try {
if (this.progressEvent(EventType.UPLOAD, `${this.startMessagePrefix} ${asset.id}`)) { return false; }
if (this.progressEvent(EventType.START, `Publishing ${asset.id}`)) { return false; }

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

this.completedOperations++;
if (this.progressEvent(EventType.SUCCESS, `${this.successMessagePrefix} ${asset.id}`)) { return false; }
if (this.progressEvent(EventType.SUCCESS, `Published ${asset.id}`)) { return false; }
} catch (e: any) {
this.failures.push({ asset, error: e });
this.completedOperations++;
Expand All @@ -212,7 +184,7 @@ export class AssetPublishing implements IPublishProgress {
*/
private async publishAsset(asset: IManifestEntry) {
try {
if (this.progressEvent(EventType.START, `${this.startMessagePrefix} ${asset.id}`)) { return false; }
if (this.progressEvent(EventType.START, `Publishing ${asset.id}`)) { return false; }

const handler = this.assetHandler(asset);

Expand All @@ -229,7 +201,7 @@ export class AssetPublishing implements IPublishProgress {
}

this.completedOperations++;
if (this.progressEvent(EventType.SUCCESS, `${this.successMessagePrefix} ${asset.id}`)) { return false; }
if (this.progressEvent(EventType.SUCCESS, `Published ${asset.id}`)) { return false; }
} catch (e: any) {
this.failures.push({ asset, error: e });
this.completedOperations++;
Expand Down
2 changes: 1 addition & 1 deletion packages/cdk-assets/test/docker-images.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ describe('with a complete manifest', () => {
test('Displays an error if the ECR repository cannot be found', async () => {
aws.mockEcr.describeImages = mockedApiFailure('RepositoryNotFoundException', 'Repository not Found');

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

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