Skip to content
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

feat: implement command destroy #18

Merged
merged 2 commits into from
Dec 9, 2024
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
7 changes: 7 additions & 0 deletions src/commands/destroy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { constructActionContext, logger, rosStackDelete } from '../common';

export const destroyStack = async (stackName: string) => {
const context = constructActionContext({ stackName });
logger.info(`Destroying stack ${stackName}...`);
await rosStackDelete(context);
};
8 changes: 8 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { logger, getVersion } from '../common';
import { validate } from './validate';
import { deploy } from './deploy';
import { template } from './template';
import { destroyStack } from './destroy';

const program = new Command();

Expand Down Expand Up @@ -62,4 +63,11 @@ program
template(stackName, { format, location: file, stage });
});

program
.command('destroy <stackName>')
.description('destroy serverless stack')
.action(async (stackName) => {
await destroyStack(stackName);
});

program.parse();
24 changes: 24 additions & 0 deletions src/common/rosClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ROS20190910, {
CreateStackRequest,
CreateStackRequestParameters,
CreateStackRequestTags,
DeleteStackRequest,
GetStackRequest,
ListStacksRequest,
UpdateStackRequest,
Expand Down Expand Up @@ -172,3 +173,26 @@ export const rosStackDeploy = async (
logger.info(`createStack success! stackName:${stack?.stackName}, stackId:${stack?.stackId}`);
}
};

export const rosStackDelete = async ({
stackName,
region,
}: Pick<ActionContext, 'stackName' | 'region'>) => {
const stackInfo = await getStackByName(stackName, region);
if (!stackInfo) {
logger.warn(`Stack: ${stackName} not exists, skipped! 🚫`);
return;
}
try {
const deleteStackRequest = new DeleteStackRequest({
regionId: region,
stackId: stackInfo.stackId,
});
await client.deleteStack(deleteStackRequest);
await getStackActionResult(stackInfo.stackId as string, region);
logger.info(`Stack: ${stackName} deleted! ♻️`);
} catch (err) {
logger.error(`Stack: ${stackName} delete failed! ❌, error: ${JSON.stringify(err)}`);
throw new Error(JSON.stringify(err));
}
};
132 changes: 87 additions & 45 deletions tests/common/rosClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { logger, rosStackDeploy } from '../../src/common';
import { logger, rosStackDelete, rosStackDeploy } from '../../src/common';
import { context } from '../fixtures/contextFixture';
import { lang } from '../../src/lang';

const mockedCreateStack = jest.fn();
const mockedUpdateStack = jest.fn();
const mockedListStacks = jest.fn();
const mockedGetStack = jest.fn();

const mockedDeleteStack = jest.fn();
jest.mock('@alicloud/ros20190910', () => ({
...jest.requireActual('@alicloud/ros20190910'),
__esModule: true,
Expand All @@ -15,6 +15,7 @@ jest.mock('@alicloud/ros20190910', () => ({
updateStack: () => mockedUpdateStack(),
listStacks: () => mockedListStacks(),
getStack: () => mockedGetStack(),
deleteStack: () => mockedDeleteStack(),
})),
}));

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

it('should create a new stack if it does not exist', async () => {
mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } });
mockedCreateStack.mockResolvedValue({ body: { stackId: 'newStackId' } });
mockedGetStack.mockResolvedValue({ body: { status: 'CREATE_COMPLETE' } });

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

expect(mockedCreateStack).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('createStack success'));
});
await rosStackDeploy('newStack', {}, context);

it('should update an existing stack if it exists', async () => {
mockedListStacks.mockResolvedValue({
statusCode: 200,
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
expect(mockedCreateStack).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('createStack success'));
});
mockedUpdateStack.mockResolvedValue({ body: { stackId: 'existingStackId' } });
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });

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

expect(mockedUpdateStack).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('stackUpdate success'));
});
await rosStackDeploy('existingStack', {}, context);

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

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

it('should notify user with warning logs when update completely same stack', async () => {
mockedListStacks.mockResolvedValue({
statusCode: 200,
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
await expect(rosStackDeploy('inProgressStack', {}, context)).rejects.toThrow(
'fail to update stack, because stack status is CREATE_IN_PROGRESS',
);
});
mockedUpdateStack.mockRejectedValueOnce({
data: { statusCode: 400, Message: 'Update the completely same stack' },

it('should notify user with warning logs when update completely same stack', async () => {
mockedListStacks.mockResolvedValue({
statusCode: 200,
body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] },
});
mockedUpdateStack.mockRejectedValueOnce({
data: { statusCode: 400, Message: 'Update the completely same stack' },
});
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });

await rosStackDeploy('existingStack', {}, context);

expect(logger.warn).toHaveBeenCalledWith(`${lang.__('UPDATE_COMPLETELY_SAME_STACK')}`);
});
mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } });

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

expect(logger.warn).toHaveBeenCalledWith(`${lang.__('UPDATE_COMPLETELY_SAME_STACK')}`);
await expect(rosStackDeploy('newStack', {}, context)).rejects.toThrow(
`Stack operation failed with status: ROLLBACK_COMPLETE`,
);
});
});

it('should throw error when deploy stack failed', async () => {
mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } });
mockedCreateStack.mockResolvedValueOnce({ body: { stackId: 'newStackId' } });
mockedGetStack.mockResolvedValue({ body: { status: 'ROLLBACK_COMPLETE' } });
describe('Unit test for rosStackDelete', () => {
it('should delete the stack when the provided stack is exists', async () => {
mockedListStacks.mockResolvedValue({
statusCode: 200,
body: { stacks: [{ stackId: 'stackToDelete', Status: 'UPDATE_COMPLETE' }] },
});
mockedDeleteStack.mockResolvedValue({ body: { stackId: 'stackToDelete' } });

await expect(rosStackDeploy('newStack', {}, context)).rejects.toThrow(
`Stack operation failed with status: ROLLBACK_COMPLETE`,
);
mockedGetStack.mockResolvedValueOnce({ body: { status: 'DELETE_COMPLETE' } });

await rosStackDelete(context);

expect(logger.info).toHaveBeenCalledWith('stack status: DELETE_COMPLETE');
expect(logger.info).toHaveBeenCalledWith('Stack: testStack deleted! ♻️');
});

it('should throw an error when the stack does not exist', async () => {
mockedListStacks.mockResolvedValue({ statusCode: 404, body: { stacks: [] } });
await rosStackDelete(context);

expect(logger.warn).toHaveBeenCalledWith('Stack: testStack not exists, skipped! 🚫');
});

it('should throw error when delete stack failed', async () => {
mockedListStacks.mockResolvedValue({
statusCode: 200,
body: { stacks: [{ stackId: 'stackToDelete', Status: 'UPDATE_COMPLETE' }] },
});
mockedDeleteStack.mockRejectedValue({ data: { statusCode: 400, Message: 'DELETE_FAILED' } });

await expect(rosStackDelete(context)).rejects.toThrow(
JSON.stringify({ statusCode: 400, Message: 'DELETE_FAILED' }),
);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Stack: testStack delete failed! ❌'),
);
});
});
});
Loading