Skip to content

Commit 7539756

Browse files
authored
fix: delete temporary resources (#55)
fix: delete temporary resources Refs: #54 --------- Signed-off-by: seven <[email protected]>
1 parent a6742c9 commit 7539756

File tree

10 files changed

+144
-43
lines changed

10 files changed

+144
-43
lines changed

src/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const CODE_ZIP_SIZE_LIMIT = 15 * 1000 * 1000;
2+
export const OSS_DEPLOYMENT_TIMEOUT = 3000; // in seconds

src/common/rosAssets.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,16 @@ const zipAssets = async (assetsPath: string) => {
6565
return zipPath;
6666
};
6767

68-
const constructAssets = async ({ files, rootPath }: CdkAssets, region: string) => {
68+
type ConstructedAsset = {
69+
bucketName: string;
70+
source: string;
71+
objectKey: string;
72+
};
73+
74+
export const constructAssets = async (
75+
{ files, rootPath }: CdkAssets,
76+
region: string,
77+
): Promise<Array<ConstructedAsset> | undefined> => {
6978
const assets = await Promise.all(
7079
Object.entries(files)
7180
.filter(([, fileItem]) => !fileItem.source.path.endsWith('.template.json'))
@@ -103,15 +112,16 @@ const ensureBucketExits = async (bucketName: string, ossClient: OSS) =>
103112
}
104113
});
105114

106-
export const publishAssets = async (assets: CdkAssets, context: ActionContext) => {
107-
const constructedAssets = await constructAssets(assets, context.region);
108-
109-
if (!constructedAssets?.length) {
115+
export const publishAssets = async (
116+
assets: Array<ConstructedAsset> | undefined,
117+
context: ActionContext,
118+
) => {
119+
if (!assets?.length) {
110120
logger.info('No assets to publish, skipped!');
111121
return;
112122
}
113123

114-
const bucketName = constructedAssets[0].bucketName;
124+
const bucketName = assets[0].bucketName;
115125

116126
const client = new OSS({
117127
region: `oss-${context.region}`,
@@ -129,11 +139,40 @@ export const publishAssets = async (assets: CdkAssets, context: ActionContext) =
129139
} as OSS.PutObjectOptions;
130140

131141
await Promise.all(
132-
constructedAssets.map(async ({ source, objectKey }) => {
142+
assets.map(async ({ source, objectKey }) => {
133143
await client.put(objectKey, path.normalize(source), { headers });
134-
logger.info(`Upload file: ${source}) to bucket: ${bucketName} successfully!`);
144+
logger.debug(`Upload file: ${source} to bucket: ${bucketName} successfully!`);
135145
}),
136146
);
137147

138148
return bucketName;
139149
};
150+
151+
export const cleanupAssets = async (
152+
assets: Array<ConstructedAsset> | undefined,
153+
context: ActionContext,
154+
) => {
155+
if (!assets?.length) {
156+
logger.info('No assets to cleanup, skipped!');
157+
return;
158+
}
159+
160+
const bucketName = assets[0].bucketName;
161+
162+
const client = new OSS({
163+
region: `oss-${context.region}`,
164+
accessKeyId: context.accessKeyId,
165+
accessKeySecret: context.accessKeySecret,
166+
bucket: bucketName,
167+
});
168+
169+
await Promise.all(
170+
assets.map(async ({ objectKey }) => {
171+
await client.delete(objectKey);
172+
logger.debug(`Cleanup file: ${objectKey} from bucket: ${bucketName} successfully!`);
173+
}),
174+
);
175+
// delete the bucket
176+
await client.deleteBucket(bucketName);
177+
logger.debug(`Cleanup bucket: ${bucketName} successfully!`);
178+
};

src/common/rosClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export const rosStackDelete = async ({
190190
});
191191
await client.deleteStack(deleteStackRequest);
192192
await getStackActionResult(stackInfo.stackId as string, region);
193-
logger.info(`Stack: ${stackName} deleted! ♻️`);
193+
logger.info(`Stack: ${stackName} deleted!🗑 `);
194194
} catch (err) {
195195
logger.error(`Stack: ${stackName} delete failed! ❌, error: ${JSON.stringify(err)}`);
196196
throw new Error(JSON.stringify(err));

src/stack/deploy.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import * as ros from '@alicloud/ros-cdk-core';
22
import fs from 'node:fs';
33

44
import { ActionContext, ServerlessIac } from '../types';
5-
import { logger, ProviderEnum, publishAssets, rosStackDeploy } from '../common';
5+
import {
6+
cleanupAssets,
7+
constructAssets,
8+
logger,
9+
ProviderEnum,
10+
publishAssets,
11+
rosStackDeploy,
12+
} from '../common';
613
import { prepareBootstrapStack, RosStack } from './rosStack';
714
import { RfsStack } from './rfsStack';
815
import { get } from 'lodash';
@@ -50,10 +57,25 @@ export const deployStack = async (
5057
const { template, assets } = generateRosStackTemplate(stackName, iac, context);
5158
await prepareBootstrapStack(context);
5259
logger.info(`Deploying stack, publishing assets...`);
53-
await publishAssets(assets, context);
54-
logger.info(`Assets published! 🎉`);
55-
await rosStackDeploy(stackName, template, context);
56-
logger.info(`Stack deployed! 🎉`);
60+
const constructedAssets = await constructAssets(assets, context.region);
61+
try {
62+
await publishAssets(constructedAssets, context);
63+
logger.info(`Assets published! 🎉`);
64+
await rosStackDeploy(stackName, template, context);
65+
} catch (e) {
66+
logger.error(`Failed to deploy stack: ${e}`);
67+
throw e;
68+
} finally {
69+
try {
70+
logger.info(`Cleaning up temporary Assets...`);
71+
await cleanupAssets(constructedAssets, context);
72+
logger.info(`Assets cleaned up!♻️`);
73+
} catch (e) {
74+
logger.error(
75+
`Failed to cleanup assets, it wont affect the deployment result, but to avoid potential cost, you can delete the temporary bucket : ${constructedAssets?.[0].bucketName}, error details:${e}`,
76+
);
77+
}
78+
}
5779
};
5880

5981
export const generateStackTemplate = (

src/stack/rosStack/bucket.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { ActionContext, BucketAccessEnum, BucketDomain } from '../../types';
22
import * as oss from '@alicloud/ros-cdk-oss';
33
import * as ros from '@alicloud/ros-cdk-core';
4-
import { encodeBase64ForRosId, getAssets, replaceReference, splitDomain } from '../../common';
4+
import {
5+
encodeBase64ForRosId,
6+
getAssets,
7+
OSS_DEPLOYMENT_TIMEOUT,
8+
replaceReference,
9+
splitDomain,
10+
} from '../../common';
511
import * as ossDeployment from '@alicloud/ros-cdk-ossdeployment';
612
import * as dns from '@alicloud/ros-cdk-dns';
713
import path from 'node:path';
@@ -80,7 +86,7 @@ export const resolveBuckets = (
8086
sources: getAssets(filePath),
8187
destinationBucket: ossBucket.attrName,
8288
roleArn: siAutoOssDeploymentBucketRole!.attrArn,
83-
timeout: 3000,
89+
timeout: OSS_DEPLOYMENT_TIMEOUT,
8490
logMonitoring: false,
8591
retainOnCreate: false,
8692
},

src/stack/rosStack/function.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CODE_ZIP_SIZE_LIMIT,
1010
encodeBase64ForRosId,
1111
getFileSource,
12+
OSS_DEPLOYMENT_TIMEOUT,
1213
readCodeSize,
1314
replaceReference,
1415
resolveCode,
@@ -143,7 +144,7 @@ export const resolveFunctions = (
143144
{
144145
sources: fileSources!.map(({ source }) => source),
145146
destinationBucket: destinationBucketName,
146-
timeout: 3000,
147+
timeout: OSS_DEPLOYMENT_TIMEOUT,
147148
logMonitoring: false,
148149
retainOnCreate: false,
149150
},

tests/common/rosAssets.test.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
import { getAssets, publishAssets } from '../../src/common';
1+
import { cleanupAssets, constructAssets, getAssets, publishAssets } from '../../src/common';
22
import { Stats } from 'node:fs';
33
import { assetsFixture } from '../fixtures/assetsFixture';
4-
import { ActionContext, CdkAssets } from '../../src/types';
4+
import { ActionContext } from '../../src/types';
5+
import { defaultContext } from '../fixtures/deployFixture';
56

67
const mockedBucketPut = jest.fn();
8+
const mockedDeleteBucket = jest.fn();
9+
const mockedDelete = jest.fn();
710
const mockedReaddirSync = jest.fn();
811
const mockedLstatSync = jest.fn();
912
const mockedExistsSync = jest.fn();
1013
const mockedGenerateAsync = jest.fn();
1114
const mockedInfoLogger = jest.fn();
15+
const mockedDebugLogger = jest.fn();
1216

1317
jest.mock('ali-oss', () =>
1418
jest.fn().mockImplementation(() => ({
1519
getBucketInfo: jest.fn().mockResolvedValue({}),
1620
put: (...args: unknown[]) => mockedBucketPut(...args),
21+
delete: (...args: unknown[]) => mockedDelete(...args),
22+
deleteBucket: (...args: unknown[]) => mockedDeleteBucket(...args),
1723
})),
1824
);
1925

@@ -43,14 +49,15 @@ jest.mock('jszip', () =>
4349
jest.mock('../../src/common/logger', () => ({
4450
logger: {
4551
info: (...args: unknown[]) => mockedInfoLogger(...args),
52+
debug: (...args: unknown[]) => mockedDebugLogger(...args),
4653
},
4754
}));
4855

4956
describe('Unit test for rosAssets', () => {
5057
beforeEach(() => {
5158
jest.clearAllMocks();
5259
});
53-
describe('Unite test for getAssets', () => {
60+
describe('Unit test for getAssets', () => {
5461
it('should return assets from the specified location', () => {
5562
const mockLocation = 'mock/location';
5663
mockedExistsSync.mockReturnValue(true);
@@ -78,7 +85,7 @@ describe('Unit test for rosAssets', () => {
7885
});
7986
});
8087

81-
describe('publishAssets', () => {
88+
describe('Unit test for publishAssets', () => {
8289
const mockContext = {
8390
region: 'mock-region',
8491
accessKeyId: 'mock-access-key-id',
@@ -93,7 +100,10 @@ describe('Unit test for rosAssets', () => {
93100
.mockReturnValueOnce({ isFile: () => true } as Stats);
94101
mockedGenerateAsync.mockResolvedValue(Buffer.from('mock-zip-content'));
95102

96-
const bucketName = await publishAssets(assetsFixture, mockContext);
103+
const bucketName = await publishAssets(
104+
await constructAssets(assetsFixture, 'mock-region'),
105+
mockContext,
106+
);
97107

98108
expect(bucketName).toBe('cdk-ajmywduza-assets-mock-region');
99109
expect(mockedBucketPut.mock.calls).toEqual([
@@ -112,21 +122,40 @@ describe('Unit test for rosAssets', () => {
112122
[
113123
'Folder compressed to: path/to/asset.55d1d2dd5d6c1b083a04c15431f70da1f2840b9de06383411cbf7eda2a512efe.zip',
114124
],
115-
[
116-
'Upload file: path/to/asset.55d1d2dd5d6c1b083a04c15431f70da1f2840b9de06383411cbf7eda2a512efe.zip) to bucket: cdk-ajmywduza-assets-mock-region successfully!',
117-
],
118-
[
119-
'Upload file: path/to/asset.c6a72ed7e7e83f01a000b75885758088fa050298a31a1e95d37ac88f08e42315.zip) to bucket: cdk-ajmywduza-assets-mock-region successfully!',
120-
],
121125
]);
122126
});
123127

124128
it('should log and skip if no assets to publish', async () => {
125-
const emptyAssets = { files: {} } as unknown as CdkAssets;
126-
127-
await publishAssets(emptyAssets, mockContext);
129+
await publishAssets([], mockContext);
128130

129131
expect(mockedInfoLogger).toHaveBeenCalledWith('No assets to publish, skipped!');
130132
});
131133
});
134+
135+
describe('Unit test for cleanupAssets', () => {
136+
it('should cleanup and delete the bucket when given assets is valid', async () => {
137+
mockedExistsSync.mockReturnValue(true);
138+
mockedReaddirSync.mockReturnValueOnce(['file1', 'file2']);
139+
mockedLstatSync
140+
.mockReturnValueOnce({ isFile: () => true } as Stats)
141+
.mockReturnValueOnce({ isFile: () => true } as Stats);
142+
mockedGenerateAsync.mockResolvedValue(Buffer.from('mock-zip-content'));
143+
144+
await cleanupAssets(await constructAssets(assetsFixture, 'mock-region'), defaultContext);
145+
146+
expect(mockedDelete).toHaveBeenCalledTimes(2);
147+
expect(mockedDelete.mock.calls).toEqual([
148+
['55d1d2dd5d6c1b083a04c15431f70da1f2840b9de06383411cbf7eda2a512efe.zip'],
149+
['c6a72ed7e7e83f01a000b75885758088fa050298a31a1e95d37ac88f08e42315.zip'],
150+
]);
151+
expect(mockedDeleteBucket).toHaveBeenCalledTimes(1);
152+
expect(mockedDeleteBucket).toHaveBeenCalledWith('cdk-ajmywduza-assets-mock-region');
153+
});
154+
155+
it('should skip the cleanupAssets when there is no assets', async () => {
156+
await cleanupAssets([], defaultContext);
157+
158+
expect(mockedInfoLogger).toHaveBeenCalledWith('No assets to cleanup, skipped!');
159+
});
160+
});
132161
});

tests/common/rosClient.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe('Unit test for rosClient', () => {
102102
await rosStackDelete(context);
103103

104104
expect(logger.info).toHaveBeenCalledWith('stack status: DELETE_COMPLETE');
105-
expect(logger.info).toHaveBeenCalledWith('Stack: testStack deleted! ♻️');
105+
expect(logger.info).toHaveBeenCalledWith('Stack: testStack deleted!🗑 ');
106106
});
107107

108108
it('should throw an error when the stack does not exist', async () => {

tests/fixtures/deployFixture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,7 +1476,7 @@ export const defaultContext = {
14761476
iacLocation: expect.stringContaining('tests/fixtures/serverless-insight.yml'),
14771477
parameters: [],
14781478
region: 'cn-hangzhou',
1479-
provider: 'aliyun',
1479+
provider: ProviderEnum.ALIYUN,
14801480
securityToken: 'account id',
14811481
stackName: 'my-demo-stack',
14821482
stage: 'default',

tests/stack/deploy.test.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,17 @@ import { cloneDeep, set } from 'lodash';
3030
const mockedRosStackDeploy = jest.fn();
3131
const mockedResolveCode = jest.fn();
3232
const mockedPublishAssets = jest.fn();
33+
const mockedCleanupAssets = jest.fn();
3334
const mockedGetIamInfo = jest.fn();
3435

3536
jest.mock('../../src/common', () => ({
3637
...jest.requireActual('../../src/common'),
3738
rosStackDeploy: (...args: unknown[]) => mockedRosStackDeploy(...args),
3839
publishAssets: (...args: unknown[]) => mockedPublishAssets(...args),
40+
cleanupAssets: (...args: unknown[]) => mockedCleanupAssets(...args),
3941
resolveCode: (path: string) => mockedResolveCode(path),
4042
getIamInfo: (...args: unknown[]) => mockedGetIamInfo(...args),
43+
logger: { info: jest.fn(), debug: jest.fn() },
4144
}));
4245

4346
describe('Unit tests for stack deployment', () => {
@@ -51,6 +54,7 @@ describe('Unit tests for stack deployment', () => {
5154
mockedResolveCode.mockRestore();
5255
mockedPublishAssets.mockRestore();
5356
mockedGetIamInfo.mockRestore();
57+
mockedCleanupAssets.mockRestore();
5458
});
5559

5660
it('should deploy generated stack when minimum fields provided', async () => {
@@ -133,25 +137,23 @@ describe('Unit tests for stack deployment', () => {
133137
{ stackName } as ActionContext,
134138
);
135139

140+
const expectedAssets = Array(2).fill({
141+
bucketName: expect.any(String),
142+
objectKey: expect.stringContaining('.zip'),
143+
source: expect.stringContaining('.zip'),
144+
});
136145
expect(mockedPublishAssets).toHaveBeenCalledTimes(1);
137146
expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2);
138-
expect(mockedPublishAssets).toHaveBeenCalledWith(
139-
expect.objectContaining({
140-
dockerImages: {},
141-
files: expect.any(Object),
142-
rootPath: expect.any(String),
143-
version: '7.0.0',
144-
}),
145-
146-
{ stackName },
147-
);
147+
expect(mockedPublishAssets).toHaveBeenCalledWith(expectedAssets, { stackName });
148148
expect(mockedRosStackDeploy.mock.calls[1]).toEqual([
149149
stackName,
150150
largeCodeRos,
151151
{
152152
stackName,
153153
},
154154
]);
155+
expect(mockedCleanupAssets).toHaveBeenCalledTimes(1);
156+
expect(mockedCleanupAssets).toHaveBeenCalledWith(expectedAssets, { stackName });
155157
});
156158

157159
describe('unit test for deploy of events', () => {
@@ -211,6 +213,7 @@ describe('Unit tests for stack deployment', () => {
211213

212214
expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2);
213215
expect(mockedPublishAssets).toHaveBeenCalledTimes(1);
216+
expect(mockedCleanupAssets).toHaveBeenCalledTimes(1);
214217
expect(mockedRosStackDeploy.mock.calls[1]).toEqual([
215218
stackName,
216219
bucketWithWebsiteRos,

0 commit comments

Comments
 (0)