Skip to content

Commit 86f240f

Browse files
authored
feat: implement command destroy (#18)
feat: implement command destroy Refs: #5 --------- Signed-off-by: seven <zilisheng1996@gmail.com>
1 parent 287ddb8 commit 86f240f

File tree

4 files changed

+126
-45
lines changed

4 files changed

+126
-45
lines changed

src/commands/destroy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { constructActionContext, logger, rosStackDelete } from '../common';
2+
3+
export const destroyStack = async (stackName: string) => {
4+
const context = constructActionContext({ stackName });
5+
logger.info(`Destroying stack ${stackName}...`);
6+
await rosStackDelete(context);
7+
};

src/commands/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logger, getVersion } from '../common';
66
import { validate } from './validate';
77
import { deploy } from './deploy';
88
import { template } from './template';
9+
import { destroyStack } from './destroy';
910

1011
const program = new Command();
1112

@@ -62,4 +63,11 @@ program
6263
template(stackName, { format, location: file, stage });
6364
});
6465

66+
program
67+
.command('destroy <stackName>')
68+
.description('destroy serverless stack')
69+
.action(async (stackName) => {
70+
await destroyStack(stackName);
71+
});
72+
6573
program.parse();

src/common/rosClient.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ROS20190910, {
33
CreateStackRequest,
44
CreateStackRequestParameters,
55
CreateStackRequestTags,
6+
DeleteStackRequest,
67
GetStackRequest,
78
ListStacksRequest,
89
UpdateStackRequest,
@@ -172,3 +173,26 @@ export const rosStackDeploy = async (
172173
logger.info(`createStack success! stackName:${stack?.stackName}, stackId:${stack?.stackId}`);
173174
}
174175
};
176+
177+
export const rosStackDelete = async ({
178+
stackName,
179+
region,
180+
}: Pick<ActionContext, 'stackName' | 'region'>) => {
181+
const stackInfo = await getStackByName(stackName, region);
182+
if (!stackInfo) {
183+
logger.warn(`Stack: ${stackName} not exists, skipped! 🚫`);
184+
return;
185+
}
186+
try {
187+
const deleteStackRequest = new DeleteStackRequest({
188+
regionId: region,
189+
stackId: stackInfo.stackId,
190+
});
191+
await client.deleteStack(deleteStackRequest);
192+
await getStackActionResult(stackInfo.stackId as string, region);
193+
logger.info(`Stack: ${stackName} deleted! ♻️`);
194+
} catch (err) {
195+
logger.error(`Stack: ${stackName} delete failed! ❌, error: ${JSON.stringify(err)}`);
196+
throw new Error(JSON.stringify(err));
197+
}
198+
};

tests/common/rosClient.test.ts

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { logger, rosStackDeploy } from '../../src/common';
1+
import { logger, rosStackDelete, rosStackDeploy } from '../../src/common';
22
import { context } from '../fixtures/contextFixture';
33
import { lang } from '../../src/lang';
44

55
const mockedCreateStack = jest.fn();
66
const mockedUpdateStack = jest.fn();
77
const mockedListStacks = jest.fn();
88
const mockedGetStack = jest.fn();
9-
9+
const mockedDeleteStack = jest.fn();
1010
jest.mock('@alicloud/ros20190910', () => ({
1111
...jest.requireActual('@alicloud/ros20190910'),
1212
__esModule: true,
@@ -15,6 +15,7 @@ jest.mock('@alicloud/ros20190910', () => ({
1515
updateStack: () => mockedUpdateStack(),
1616
listStacks: () => mockedListStacks(),
1717
getStack: () => mockedGetStack(),
18+
deleteStack: () => mockedDeleteStack(),
1819
})),
1920
}));
2021

@@ -25,64 +26,105 @@ describe('Unit test for rosClient', () => {
2526
jest.clearAllMocks();
2627
});
2728

28-
it('should create a new stack if it does not exist', async () => {
29-
mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } });
30-
mockedCreateStack.mockResolvedValue({ body: { stackId: 'newStackId' } });
31-
mockedGetStack.mockResolvedValue({ body: { status: 'CREATE_COMPLETE' } });
32-
33-
await rosStackDeploy('newStack', {}, context);
29+
describe('Unit tes for rosStackDeploy', () => {
30+
it('should create a new stack if it does not exist', async () => {
31+
mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } });
32+
mockedCreateStack.mockResolvedValue({ body: { stackId: 'newStackId' } });
33+
mockedGetStack.mockResolvedValue({ body: { status: 'CREATE_COMPLETE' } });
3434

35-
expect(mockedCreateStack).toHaveBeenCalled();
36-
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('createStack success'));
37-
});
35+
await rosStackDeploy('newStack', {}, context);
3836

39-
it('should update an existing stack if it exists', async () => {
40-
mockedListStacks.mockResolvedValue({
41-
statusCode: 200,
42-
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
37+
expect(mockedCreateStack).toHaveBeenCalled();
38+
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('createStack success'));
4339
});
44-
mockedUpdateStack.mockResolvedValue({ body: { stackId: 'existingStackId' } });
45-
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });
4640

47-
await rosStackDeploy('existingStack', {}, context);
41+
it('should update an existing stack if it exists', async () => {
42+
mockedListStacks.mockResolvedValue({
43+
statusCode: 200,
44+
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
45+
});
46+
mockedUpdateStack.mockResolvedValue({ body: { stackId: 'existingStackId' } });
47+
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });
4848

49-
expect(mockedUpdateStack).toHaveBeenCalled();
50-
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('stackUpdate success'));
51-
});
49+
await rosStackDeploy('existingStack', {}, context);
5250

53-
it('should throw an error if the stack is in progress', async () => {
54-
mockedListStacks.mockResolvedValue({
55-
statusCode: 200,
56-
body: { stacks: [{ stackId: 'inProgressStackId', Status: 'CREATE_IN_PROGRESS' }] },
51+
expect(mockedUpdateStack).toHaveBeenCalled();
52+
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('stackUpdate success'));
5753
});
5854

59-
await expect(rosStackDeploy('inProgressStack', {}, context)).rejects.toThrow(
60-
'fail to update stack, because stack status is CREATE_IN_PROGRESS',
61-
);
62-
});
55+
it('should throw an error if the stack is in progress', async () => {
56+
mockedListStacks.mockResolvedValue({
57+
statusCode: 200,
58+
body: { stacks: [{ stackId: 'inProgressStackId', Status: 'CREATE_IN_PROGRESS' }] },
59+
});
6360

64-
it('should notify user with warning logs when update completely same stack', async () => {
65-
mockedListStacks.mockResolvedValue({
66-
statusCode: 200,
67-
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
61+
await expect(rosStackDeploy('inProgressStack', {}, context)).rejects.toThrow(
62+
'fail to update stack, because stack status is CREATE_IN_PROGRESS',
63+
);
6864
});
69-
mockedUpdateStack.mockRejectedValueOnce({
70-
data: { statusCode: 400, Message: 'Update the completely same stack' },
65+
66+
it('should notify user with warning logs when update completely same stack', async () => {
67+
mockedListStacks.mockResolvedValue({
68+
statusCode: 200,
69+
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
70+
});
71+
mockedUpdateStack.mockRejectedValueOnce({
72+
data: { statusCode: 400, Message: 'Update the completely same stack' },
73+
});
74+
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });
75+
76+
await rosStackDeploy('existingStack', {}, context);
77+
78+
expect(logger.warn).toHaveBeenCalledWith(`${lang.__('UPDATE_COMPLETELY_SAME_STACK')}`);
7179
});
72-
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });
7380

74-
await rosStackDeploy('existingStack', {}, context);
81+
it('should throw error when deploy stack failed', async () => {
82+
mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } });
83+
mockedCreateStack.mockResolvedValueOnce({ body: { stackId: 'newStackId' } });
84+
mockedGetStack.mockResolvedValue({ body: { status: 'ROLLBACK_COMPLETE' } });
7585

76-
expect(logger.warn).toHaveBeenCalledWith(`${lang.__('UPDATE_COMPLETELY_SAME_STACK')}`);
86+
await expect(rosStackDeploy('newStack', {}, context)).rejects.toThrow(
87+
`Stack operation failed with status: ROLLBACK_COMPLETE`,
88+
);
89+
});
7790
});
7891

79-
it('should throw error when deploy stack failed', async () => {
80-
mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } });
81-
mockedCreateStack.mockResolvedValueOnce({ body: { stackId: 'newStackId' } });
82-
mockedGetStack.mockResolvedValue({ body: { status: 'ROLLBACK_COMPLETE' } });
92+
describe('Unit test for rosStackDelete', () => {
93+
it('should delete the stack when the provided stack is exists', async () => {
94+
mockedListStacks.mockResolvedValue({
95+
statusCode: 200,
96+
body: { stacks: [{ stackId: 'stackToDelete', Status: 'UPDATE_COMPLETE' }] },
97+
});
98+
mockedDeleteStack.mockResolvedValue({ body: { stackId: 'stackToDelete' } });
8399

84-
await expect(rosStackDeploy('newStack', {}, context)).rejects.toThrow(
85-
`Stack operation failed with status: ROLLBACK_COMPLETE`,
86-
);
100+
mockedGetStack.mockResolvedValueOnce({ body: { status: 'DELETE_COMPLETE' } });
101+
102+
await rosStackDelete(context);
103+
104+
expect(logger.info).toHaveBeenCalledWith('stack status: DELETE_COMPLETE');
105+
expect(logger.info).toHaveBeenCalledWith('Stack: testStack deleted! ♻️');
106+
});
107+
108+
it('should throw an error when the stack does not exist', async () => {
109+
mockedListStacks.mockResolvedValue({ statusCode: 404, body: { stacks: [] } });
110+
await rosStackDelete(context);
111+
112+
expect(logger.warn).toHaveBeenCalledWith('Stack: testStack not exists, skipped! 🚫');
113+
});
114+
115+
it('should throw error when delete stack failed', async () => {
116+
mockedListStacks.mockResolvedValue({
117+
statusCode: 200,
118+
body: { stacks: [{ stackId: 'stackToDelete', Status: 'UPDATE_COMPLETE' }] },
119+
});
120+
mockedDeleteStack.mockRejectedValue({ data: { statusCode: 400, Message: 'DELETE_FAILED' } });
121+
122+
await expect(rosStackDelete(context)).rejects.toThrow(
123+
JSON.stringify({ statusCode: 400, Message: 'DELETE_FAILED' }),
124+
);
125+
expect(logger.error).toHaveBeenCalledWith(
126+
expect.stringContaining('Stack: testStack delete failed! ❌'),
127+
);
128+
});
87129
});
88130
});

0 commit comments

Comments
 (0)