Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ integTest('cdk destroy prompts the user for confirmation', withDefaultFixture(as
interact: [
{ prompt: /Are you sure you want to delete/, input: 'no' },
],
modEnv: {
// disable coloring because it messes up prompt matching.
FORCE_COLOR: '0',
},
});

// assert we didn't destroy the stack
Expand Down
3 changes: 1 addition & 2 deletions packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import type {
AssemblyData,
ConfirmationRequest,
ContextProviderMessageSource,
DataRequest,
Duration,
ErrorPayload,
SingleStack,
Expand Down Expand Up @@ -391,7 +390,7 @@ export const IO = {
interface: 'RefactorResult',
}),

CDK_TOOLKIT_I8910: make.question<DataRequest>({
CDK_TOOLKIT_I8910: make.confirm<ConfirmationRequest>({
code: 'CDK_TOOLKIT_I8910',
description: 'Confirm refactor',
interface: 'ConfirmationRequest',
Expand Down
7 changes: 3 additions & 4 deletions packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1190,10 +1190,9 @@ export class Toolkit extends CloudAssemblySourceBuilder {
}

const question = 'Do you wish to refactor these resources?';
const response = await ioHelper.requestResponse(IO.CDK_TOOLKIT_I8910.req(question, {
responseDescription: '[Y]es/[n]o',
}, 'y'));
return ['y', 'yes'].includes(response.toLowerCase());
return ioHelper.requestResponse(IO.CDK_TOOLKIT_I8910.req(question, {
motivation: 'User input is needed',
}));
}

function formatError(error: any): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ describe('refactor execution', () => {
expect.objectContaining({
action: 'refactor',
code: 'CDK_TOOLKIT_I8910',
defaultResponse: 'y',
defaultResponse: true,
level: 'info',
message: 'Do you wish to refactor these resources?',
}),
Expand Down
80 changes: 34 additions & 46 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import * as path from 'path';
import { format } from 'util';
import { RequireApproval } from '@aws-cdk/cloud-assembly-schema';
import * as cxapi from '@aws-cdk/cx-api';
import type { DeploymentMethod, ToolkitAction, ToolkitOptions } from '@aws-cdk/toolkit-lib';
import type { ConfirmationRequest, DeploymentMethod, ToolkitAction, ToolkitOptions } from '@aws-cdk/toolkit-lib';
import { PermissionChangeType, Toolkit, ToolkitError } from '@aws-cdk/toolkit-lib';
import * as chalk from 'chalk';
import * as chokidar from 'chokidar';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
import * as uuid from 'uuid';
import { CliIoHost } from './io-host';
import type { Configuration } from './user-configuration';
import { PROJECT_CONFIG } from './user-configuration';
import type { IoHelper } from '../../lib/api-private';
import { asIoHelper, cfnApi, tagsForStack } from '../../lib/api-private';
import type { ActionLessRequest, IoHelper } from '../../lib/api-private';
import { asIoHelper, cfnApi, IO, tagsForStack } from '../../lib/api-private';
import type { AssetBuildNode, AssetPublishNode, Concurrency, StackNode, WorkGraph } from '../api';
import {
CloudWatchLogEventMonitor,
Expand Down Expand Up @@ -74,12 +73,6 @@ import type { ErrorDetails } from './telemetry/schema';
// eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/consistent-type-imports
const pLimit: typeof import('p-limit') = require('p-limit');

let TESTING = false;

export function markTesting() {
TESTING = true;
}

export interface CdkToolkitProps {
/**
* The Cloud Executable
Expand Down Expand Up @@ -495,12 +488,16 @@ export class CdkToolkit {
});
const securityDiff = formatter.formatSecurityDiff();
if (requiresApproval(requireApproval, securityDiff.permissionChangeType)) {
const motivation = '"--require-approval" is enabled and stack includes security-sensitive updates';
await this.ioHost.asIoHelper().defaults.info(securityDiff.formattedDiff);
await askUserConfirmation(
this.ioHost,
concurrency,
'"--require-approval" is enabled and stack includes security-sensitive updates',
'Do you wish to deploy these changes',
IO.CDK_TOOLKIT_I5060.req(`${motivation}: 'Do you wish to deploy these changes'`, {
motivation,
concurrency,
permissionChangeType: securityDiff.permissionChangeType,
templateDiffs: formatter.diffs,
}),
);
}
}
Expand Down Expand Up @@ -578,9 +575,10 @@ export class CdkToolkit {
} else {
await askUserConfirmation(
this.ioHost,
concurrency,
motivation,
`${motivation}. Roll back first and then proceed with deployment`,
IO.CDK_TOOLKIT_I5050.req(`${motivation}. Roll back first and then proceed with deployment`, {
motivation,
concurrency,
}),
);
}

Expand All @@ -604,9 +602,10 @@ export class CdkToolkit {
} else {
await askUserConfirmation(
this.ioHost,
concurrency,
motivation,
`${motivation}. Perform a regular deployment`,
IO.CDK_TOOLKIT_I5050.req(`${motivation}. Perform a regular deployment`, {
concurrency,
motivation,
}),
);
}

Expand Down Expand Up @@ -970,33 +969,37 @@ export class CdkToolkit {
}

public async destroy(options: DestroyOptions) {
let stacks = await this.selectStacksForDestroy(options.selector, options.exclusively);
const ioHelper = this.ioHost.asIoHelper();

// The stacks will have been ordered for deployment, so reverse them for deletion.
stacks = stacks.reversed();
const stacks = (await this.selectStacksForDestroy(options.selector, options.exclusively)).reversed();

if (!options.force) {
// eslint-disable-next-line @stylistic/max-len
const confirmed = await promptly.confirm(
`Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))} (y/n)?`,
);
if (!confirmed) {
const motivation = 'Destroying stacks is an irreversible action';
const question = `Are you sure you want to delete: ${chalk.blue(stacks.stackArtifacts.map((s) => s.hierarchicalId).join(', '))}`;
try {
await ioHelper.requestResponse(IO.CDK_TOOLKIT_I7010.req(question, { motivation }));
} catch (err: unknown) {
if (!ToolkitError.isToolkitError(err) || err.message != 'Aborted by user') {
throw err; // unexpected error
}
await ioHelper.notify(IO.CDK_TOOLKIT_E7010.msg(err.message));
return;
}
}

const action = options.fromDeploy ? 'deploy' : 'destroy';
for (const [index, stack] of stacks.stackArtifacts.entries()) {
await this.ioHost.asIoHelper().defaults.info(chalk.green('%s: destroying... [%s/%s]'), chalk.blue(stack.displayName), index + 1, stacks.stackCount);
await ioHelper.defaults.info(chalk.green('%s: destroying... [%s/%s]'), chalk.blue(stack.displayName), index + 1, stacks.stackCount);
try {
await this.props.deployments.destroyStack({
stack,
deployName: stack.stackName,
roleArn: options.roleArn,
});
await this.ioHost.asIoHelper().defaults.info(chalk.green(`\n ✅ %s: ${action}ed`), chalk.blue(stack.displayName));
await ioHelper.defaults.info(chalk.green(`\n ✅ %s: ${action}ed`), chalk.blue(stack.displayName));
} catch (e) {
await this.ioHost.asIoHelper().defaults.error(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
await ioHelper.defaults.error(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
throw e;
}
}
Expand Down Expand Up @@ -2103,25 +2106,10 @@ function buildParameterMap(
*/
async function askUserConfirmation(
ioHost: CliIoHost,
concurrency: number,
motivation: string,
question: string,
req: ActionLessRequest<ConfirmationRequest, boolean>,
) {
await ioHost.withCorkedLogging(async () => {
// only talk to user if STDIN is a terminal (otherwise, fail)
if (!TESTING && !process.stdin.isTTY) {
throw new ToolkitError(`${motivation}, 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 ToolkitError(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`);
}

const confirmed = await promptly.confirm(`${chalk.cyan(question)} (y/n)?`);
if (!confirmed) {
throw new ToolkitError('Aborted by user');
}
await ioHost.asIoHelper().requestResponse(req);
});
}

Expand Down
13 changes: 6 additions & 7 deletions packages/aws-cdk/test/cli/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import type { DestroyStackResult } from '@aws-cdk/toolkit-lib/lib/api/deployment
import { DescribeStacksCommand, GetTemplateCommand, StackStatus } from '@aws-sdk/client-cloudformation';
import { GetParameterCommand } from '@aws-sdk/client-ssm';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
import type { Template, SdkProvider } from '../../lib/api';
import { Bootstrapper, type BootstrapSource } from '../../lib/api/bootstrap';
import type {
Expand All @@ -81,7 +80,7 @@ import {
import { Mode } from '../../lib/api/plugin';
import type { Tag } from '../../lib/api/tags';
import { asIoHelper } from '../../lib/api-private';
import { CdkToolkit, markTesting } from '../../lib/cli/cdk-toolkit';
import { CdkToolkit } from '../../lib/cli/cdk-toolkit';
import { CliIoHost } from '../../lib/cli/io-host';
import { Configuration } from '../../lib/cli/user-configuration';
import { StackActivityProgress } from '../../lib/commands/deploy';
Expand All @@ -99,14 +98,13 @@ import {
} from '../_helpers/mock-sdk';
import { promiseWithResolvers } from '../_helpers/promises';

markTesting();

const defaultBootstrapSource: BootstrapSource = { source: 'default' };
const bootstrapEnvironmentMock = jest.spyOn(Bootstrapper.prototype, 'bootstrapEnvironment');
let cloudExecutable: MockCloudExecutable;
let ioHost = CliIoHost.instance();
let ioHelper = asIoHelper(ioHost, 'deploy');
let notifySpy = jest.spyOn(ioHost, 'notify');
let requestSpy = jest.spyOn(ioHost, 'requestResponse');

beforeEach(async () => {
jest.resetAllMocks();
Expand Down Expand Up @@ -1700,7 +1698,8 @@ describe('rollback', () => {
stackArn: 'stack:arn',
});

const mockedConfirm = jest.spyOn(promptly, 'confirm').mockResolvedValue(true);
// respond with yes
requestSpy.mockImplementationOnce(async () => true);

const toolkit = new CdkToolkit({
ioHost,
Expand All @@ -1725,9 +1724,9 @@ describe('rollback', () => {
if (!useForce) {
// Questions will have been asked only if --force is not specified
if (firstResult.type === 'failpaused-need-rollback-first') {
expect(mockedConfirm).toHaveBeenCalledWith(expect.stringContaining('Roll back first and then proceed with deployment'));
expect(requestSpy).toHaveBeenCalledWith(expectIoMsg(expect.stringContaining('Roll back first and then proceed with deployment')));
} else {
expect(mockedConfirm).toHaveBeenCalledWith(expect.stringContaining('Perform a regular deployment'));
expect(requestSpy).toHaveBeenCalledWith(expectIoMsg(expect.stringContaining('Perform a regular deployment')));
}
}

Expand Down
Loading