Skip to content

Commit

Permalink
chore(toolkit): simple prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
mrgrain committed Jan 15, 2025
1 parent 2066b52 commit 7feaa11
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 39 deletions.
3 changes: 2 additions & 1 deletion packages/@aws-cdk/toolkit/lib/actions/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Deployments } from 'aws-cdk/lib/api/deployments';
import { StackActivityProgress } from 'aws-cdk/lib/api/util/cloudformation/stack-activity-monitor';
import { WorkGraph } from 'aws-cdk/lib/util/work-graph';
import { StackSelector } from '../types';
import { StackActivityProgress } from 'aws-cdk/lib/api/util/cloudformation/stack-activity-monitor';

export type DeploymentMethod = DirectDeploymentMethod | ChangeSetDeploymentMethod;

Expand Down Expand Up @@ -133,6 +133,7 @@ export interface BaseDeployOptions {
* Always deploy, even if templates are identical.
*
* @default false
* @deprecated
*/
readonly force?: boolean;

Expand Down
58 changes: 45 additions & 13 deletions packages/@aws-cdk/toolkit/lib/io/messages.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import * as chalk from 'chalk';
import { IoMessage, IoMessageCode, IoMessageCodeCategory, IoMessageLevel } from './io-host';
import { IoMessage, IoMessageCode, IoMessageCodeCategory, IoMessageLevel, IoRequest } from './io-host';

type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;

type SimplifiedMessage<T> = Pick<IoMessage<T>, 'level' | 'code' | 'message' | 'data'>;
type ActionLessMessage<T> = Omit<IoMessage<T>, 'action'>;
type ActionLessRequest<T, U> = Omit<IoRequest<T, U>, 'action'>;

/**
* Internal helper that processes log inputs into a consistent format.
* Handles string interpolation, format strings, and object parameter styles.
* Applies optional styling and prepares the final message for logging.
*/
function formatMessage<T>(
msg: Pick<Optional<IoMessage<T>, 'code'>, 'level' | 'code' | 'message' | 'data'>,
style?: (str: string) => string,
): Omit<IoMessage<T>, 'action'> {
// Apply style if provided
const formattedMessage = style ? style(msg.message) : msg.message;

function formatMessage<T>(msg: Optional<SimplifiedMessage<T>, 'code'>): ActionLessMessage<T> {
return {
time: new Date(),
level: msg.level,
code: msg.code ?? messageCode(msg.level),
message: formattedMessage,
message: msg.message,
data: msg.data,
};
}
Expand All @@ -34,6 +32,40 @@ function messageCode(level: IoMessageLevel, category: IoMessageCodeCategory = 'T
return `CDK_${category}_${levelIndicator}${number ?? '0000'}`;
}

/**
* Requests a yes/no confirmation from the IoHost.
*/
export const confirm = (
code: IoMessageCode,
question: string,
motivation: string,
defaultResponse: boolean,
concurrency?: number,
): ActionLessRequest<{
motivation: string;
concurrency?: number;
}, boolean> => {
return prompt(code, `${chalk.cyan(question)} (y/n)?`, defaultResponse, {
motivation,
concurrency,
});
};

/**
* Prompt for a a response from the IoHost.
*/
export const prompt = <T, U>(code: IoMessageCode, message: string, defaultResponse: U, payload?: T): ActionLessRequest<T, U> => {
return {
defaultResponse,
...formatMessage({
level: 'info',
code,
message,
data: payload,
}),
};
};

/**
* Logs an error level message.
*/
Expand Down Expand Up @@ -115,9 +147,9 @@ export const success = <T>(message: string, code?: IoMessageCode, payload?: T) =
return formatMessage({
level: 'info',
code,
message,
message: chalk.green(message),
data: payload,
}, chalk.green);
});
};

/**
Expand All @@ -128,7 +160,7 @@ export const highlight = <T>(message: string, code?: IoMessageCode, payload?: T)
return formatMessage({
level: 'info',
code,
message,
message: chalk.bold(message),
data: payload,
}, chalk.bold);
});
};
50 changes: 25 additions & 25 deletions packages/@aws-cdk/toolkit/lib/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { ICloudAssemblySource } from './api/cloud-assembly/types';
import { ToolkitError } from './api/errors';
import { IIoHost } from './io/io-host';
import { asSdkLogger, withAction } from './io/logger';
import { data, error, highlight, info, success, warning } from './io/messages';
import { confirm, data, error, highlight, info, success, warning } from './io/messages';
import { Timer } from './io/timer';
import { StackSelectionStrategy, ToolkitAction } from './types';

Expand Down Expand Up @@ -309,15 +309,14 @@ export class Toolkit {
const motivation = r.reason === 'replacement'
? `Stack is in a paused fail state (${r.status}) and change includes a replacement which cannot be deployed with "--no-rollback"`
: `Stack is in a paused fail state (${r.status}) and command line arguments do not include "--no-rollback"`;
const question = `${motivation}. Perform a regular deployment`;

if (options.force) {
await ioHost.notify(warning(`${motivation}. Rolling back first (--force).`));
} else {
await askUserConfirmation(
concurrency,
motivation,
`${motivation}. Roll back first and then proceed with deployment`,
);
// @todo reintroduce concurrency and corked logging in CliHost
const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I5050', question, motivation, true, concurrency));
if (!confirmed) { throw new ToolkitError('Aborted by user'); }
}

// Perform a rollback
Expand All @@ -334,15 +333,15 @@ export class Toolkit {

case 'replacement-requires-rollback': {
const motivation = 'Change includes a replacement which cannot be deployed with "--no-rollback"';
const question = `${motivation}. Perform a regular deployment`;

// @todo no force here
if (options.force) {
await ioHost.notify(warning(`${motivation}. Proceeding with regular deployment (--force).`));
} else {
await askUserConfirmation(
concurrency,
motivation,
`${motivation}. Perform a regular deployment`,
);
// @todo reintroduce concurrency and corked logging in CliHost
const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I5050', question, motivation, true, concurrency));
if (!confirmed) { throw new ToolkitError('Aborted by user'); }
}

// Go around through the 'while' loop again but switch rollback to false.
Expand Down Expand Up @@ -384,14 +383,16 @@ export class Toolkit {
[`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), e.message].join(' '),
);
} finally {
if (options.cloudWatchLogMonitor) {
const foundLogGroupsResult = await findCloudWatchLogGroups(await this.sdkProvider('deploy'), stack);
options.cloudWatchLogMonitor.addLogGroups(
foundLogGroupsResult.env,
foundLogGroupsResult.sdk,
foundLogGroupsResult.logGroupNames,
);
}
// @todo
// if (options.cloudWatchLogMonitor) {
// const foundLogGroupsResult = await findCloudWatchLogGroups(await this.sdkProvider('deploy'), stack);
// options.cloudWatchLogMonitor.addLogGroups(
// foundLogGroupsResult.env,
// foundLogGroupsResult.sdk,
// foundLogGroupsResult.logGroupNames,
// );
// }

// If an outputs file has been specified, create the file path and write stack outputs to it once.
// Outputs are written after all stacks have been deployed. If a stack deployment fails,
// all of the outputs from successfully deployed stacks before the failure will still be written.
Expand Down Expand Up @@ -474,15 +475,14 @@ export class Toolkit {
*/
private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise<void> {
const ioHost = withAction(this.ioHost, action);
let stacks = await assembly.selectStacksV2(options.stacks);

// The stacks will have been ordered for deployment, so reverse them for deletion.
stacks = stacks.reversed();
const stacks = await assembly.selectStacksV2(options.stacks).reversed();

const msg = `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))} (y/n)?`;
const confirmed = await this.ioHost.requestResponse<any, boolean>(prompt(msg, true));
const motivation = 'Destroying stacks is an irreversible action';
const question = `Are you sure you want to delete: ${chalk.red(stacks.hierarchicalIds.join(', '))}`;
const confirmed = await ioHost.requestResponse(confirm('CDK_TOOLKIT_I7010', question, motivation, true));
if (!confirmed) {
return;
return ioHost.notify(error('Aborted by user'));
}

for (const [index, stack] of stacks.stackArtifacts.entries()) {
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ export class StackCollection {
return this.stackArtifacts.map(s => s.id);
}

public get hierarchicalIds(): string[] {
return this.stackArtifacts.map(s => s.hierarchicalId);

Check warning on line 235 in packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts

View check run for this annotation

Codecov / codecov/patch

packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts#L234-L235

Added lines #L234 - L235 were not covered by tests
}

public reversed() {
const arts = [...this.stackArtifacts];
arts.reverse();
Expand Down

0 comments on commit 7feaa11

Please sign in to comment.