Skip to content

Commit

Permalink
Error message for destroy action on non-existent stack
Browse files Browse the repository at this point in the history
  • Loading branch information
slxsh committed May 29, 2024
1 parent f470271 commit 262c58a
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 16 deletions.
8 changes: 7 additions & 1 deletion packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ export async function destroyStack(options: DestroyStackOptions) {

const currentStack = await CloudFormationStack.lookup(cfn, deployName);
if (!currentStack.exists) {
return;
throw new StackNotFoundError(`Failed to destroy ${deployName}. The stack is not deployed.`);
}
const monitor = options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, {
ci: options.ci,
Expand Down Expand Up @@ -694,3 +694,9 @@ function suffixWithErrors(msg: string, errors?: string[]) {
? `${msg}: ${errors.join(', ')}`
: msg;
}

export class StackNotFoundError extends Error {
constructor(message: string) {
super(message);
}
}
12 changes: 8 additions & 4 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as chokidar from 'chokidar';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
import * as uuid from 'uuid';
import { DeploymentMethod } from './api';
import { DeploymentMethod, StackNotFoundError } from './api';
import { SdkProvider } from './api/aws-auth';
import { Bootstrapper, BootstrapEnvironmentOptions } from './api/bootstrap';
import { CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollection, StackSelector } from './api/cxapp/cloud-assembly';
Expand Down Expand Up @@ -620,8 +620,12 @@ export class CdkToolkit {
});
success(`\n ✅ %s: ${action}ed`, chalk.blue(stack.displayName));
} catch (e) {
error(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
throw e;
if (e instanceof StackNotFoundError) {
error(`\n ❌ %s: ${action} skipped`, chalk.blue(stack.displayName), e);
} else {
error(`\n ❌ %s: ${action} failed`, chalk.blue(stack.displayName), e);
throw e;
}
}
}
}
Expand Down Expand Up @@ -1525,4 +1529,4 @@ function buildParameterMap(parameters: {
}

return parameterMap;
}
}
14 changes: 12 additions & 2 deletions packages/aws-cdk/test/api/deploy-stack.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable import/order */
import { deployStack, DeployStackOptions } from '../../lib/api';
import { deployStack, DeployStackOptions, destroyStack } from '../../lib/api';
import { HotswapMode } from '../../lib/api/hotswap/common';
import { tryHotswapDeployment } from '../../lib/api/hotswap-deployments';
import { setCI } from '../../lib/logging';
Expand Down Expand Up @@ -630,6 +630,16 @@ test('deploy is not skipped if stack is in a _FAILED state', async () => {
expect(cfnMocks.createChangeSet).toHaveBeenCalled();
});

test('destroy stack throws error if stack is not deployed', async () => {
await expect(destroyStack({
stack: FAKE_STACK,
sdk: sdk,
deployName: FAKE_STACK.stackName,
})).rejects.toThrow(`Failed to destroy ${FAKE_STACK.stackName}. The stack is not deployed.`);

expect(cfnMocks.deleteStack).not.toHaveBeenCalled();
});

test('existing stack in UPDATE_ROLLBACK_COMPLETE state can be updated', async () => {
// GIVEN
givenStackExists(
Expand Down Expand Up @@ -915,4 +925,4 @@ function givenTemplateIs(template: any) {
cfnMocks.getTemplate!.mockReturnValue({
TemplateBody: JSON.stringify(template),
});
}
}
40 changes: 31 additions & 9 deletions packages/aws-cdk/test/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import * as fs from 'fs-extra';
import { instanceMockFrom, MockCloudExecutable, TestStackArtifact } from './util';
import { MockSdkProvider } from './util/mock-sdk';
import { Bootstrapper } from '../lib/api/bootstrap';
import { DeployStackResult } from '../lib/api/deploy-stack';
import { DeployStackResult, StackNotFoundError } from '../lib/api/deploy-stack';
import { Deployments, DeployStackOptions, DestroyStackOptions } from '../lib/api/deployments';
import { HotswapMode } from '../lib/api/hotswap/common';
import { Template } from '../lib/api/util/cloudformation';
Expand Down Expand Up @@ -591,14 +591,36 @@ describe('destroy', () => {
test('destroy correct stack', async () => {
const toolkit = defaultToolkitSetup();

await expect(() => {
return toolkit.destroy({
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
exclusively: true,
force: true,
fromDeploy: true,
});
}).resolves;
expect(toolkit.destroy({
selector: { patterns: ['Test-Stack-A/Test-Stack-C'] },
exclusively: true,
force: true,
fromDeploy: true,
})).resolves;
});

test('destroy keeps running when one of the stacks is not deployed', async () => {
const toolkit = defaultToolkitSetup();
jest.spyOn(FakeCloudFormation.prototype, 'destroyStack').mockImplementationOnce(() => Promise.reject(new StackNotFoundError('')));

expect(toolkit.destroy({
selector: { patterns: ['Test-Stack-A', 'Test-Stack-B', 'Test-Stack-C'] },
exclusively: true,
force: true,
fromDeploy: true,
})).resolves;
});

test('destroy stops if actual error occurs', async () => {
const toolkit = defaultToolkitSetup();
jest.spyOn(FakeCloudFormation.prototype, 'destroyStack').mockImplementationOnce(() => Promise.reject(new Error('Some error')));

await expect(toolkit.destroy({
selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] },
exclusively: true,
force: true,
fromDeploy: true,
})).rejects.toThrow('Some error');
});
});

Expand Down

0 comments on commit 262c58a

Please sign in to comment.