Skip to content

Commit 93974f3

Browse files
authored
feat: enable support for define oss resources (#37)
feat: enable support for define oss resources Refs: #36 --------- Signed-off-by: seven <[email protected]>
1 parent 14dab09 commit 93974f3

File tree

19 files changed

+10588
-5661
lines changed

19 files changed

+10588
-5661
lines changed

package-lock.json

Lines changed: 9731 additions & 5582 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,25 @@
5151
"dependencies": {
5252
"@alicloud/ims20190815": "^2.1.4",
5353
"@alicloud/openapi-client": "^0.4.12",
54-
"@alicloud/ros-cdk-apigateway": "^1.4.0",
55-
"@alicloud/ros-cdk-core": "^1.5.0",
56-
"@alicloud/ros-cdk-elasticsearchserverless": "^1.4.0",
57-
"@alicloud/ros-cdk-fc3": "^1.5.0",
58-
"@alicloud/ros-cdk-oss": "^1.4.0",
59-
"@alicloud/ros-cdk-ossdeployment": "^1.4.0",
60-
"@alicloud/ros-cdk-ram": "^1.4.0",
61-
"@alicloud/ros-cdk-rds": "^1.5.0",
62-
"@alicloud/ros-cdk-sls": "^1.5.0",
54+
"@alicloud/ros-cdk-apigateway": "^1.6.0",
55+
"@alicloud/ros-cdk-core": "^1.6.0",
56+
"@alicloud/ros-cdk-elasticsearchserverless": "^1.6.0",
57+
"@alicloud/ros-cdk-fc3": "^1.6.0",
58+
"@alicloud/ros-cdk-oss": "^1.6.0",
59+
"@alicloud/ros-cdk-ossdeployment": "^1.6.0",
60+
"@alicloud/ros-cdk-ram": "^1.6.0",
61+
"@alicloud/ros-cdk-rds": "^1.6.0",
62+
"@alicloud/ros-cdk-sls": "^1.6.0",
6363
"@alicloud/ros20190910": "^3.5.2",
6464
"ajv": "^8.17.1",
6565
"ali-oss": "^6.22.0",
6666
"chalk": "^5.4.1",
6767
"commander": "^12.1.0",
68+
"i": "^0.3.7",
6869
"i18n": "^0.15.1",
70+
"jszip": "^3.10.1",
6971
"lodash": "^4.17.21",
72+
"npm": "^11.1.0",
7073
"pino": "^9.5.0",
7174
"pino-pretty": "^13.0.0",
7275
"yaml": "^2.6.1"

src/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './iacHelper';
77
export * from './constants';
88
export * from './imsClient';
99
export * from './base64';
10+
export * from './rosAssets';

src/common/rosAssets.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { ISource } from '@alicloud/ros-cdk-ossdeployment/lib/source.cdk';
2+
import fs from 'node:fs';
3+
import * as ossDeployment from '@alicloud/ros-cdk-ossdeployment';
4+
import path from 'node:path';
5+
import JSZip from 'jszip';
6+
import { logger } from './logger';
7+
import { ActionContext, CdkAssets } from '../types';
8+
import { get, isEmpty } from 'lodash';
9+
import OSS from 'ali-oss';
10+
11+
const buildAssets = (rootPath: string, relativePath: string): Array<ISource> => {
12+
const location = path.resolve(rootPath, relativePath);
13+
if (!fs.existsSync(location)) {
14+
throw new Error(`Location: ${location} is not exists!`);
15+
}
16+
if (fs.lstatSync(location).isFile()) {
17+
return [
18+
ossDeployment.Source.asset(
19+
location,
20+
{},
21+
relativePath.substring(0, relativePath.lastIndexOf('/') + 1),
22+
),
23+
];
24+
}
25+
return fs
26+
.readdirSync(location)
27+
.map((file) => buildAssets(rootPath, `${relativePath}/${file}`.replace(/^\//, '')))
28+
.flat();
29+
};
30+
31+
export const getAssets = (location: string): Array<ISource> => {
32+
return buildAssets(location, '');
33+
};
34+
35+
const assembleFiles = (folder: string, zip: JSZip) => {
36+
const files = fs.readdirSync(folder);
37+
files.forEach((file) => {
38+
const filePath = path.join(folder, file);
39+
if (fs.lstatSync(filePath).isFile()) {
40+
const content = fs.readFileSync(filePath);
41+
zip.file(file, content);
42+
} else {
43+
const subZip = zip.folder(file);
44+
if (subZip) {
45+
assembleFiles(filePath, subZip);
46+
}
47+
}
48+
});
49+
};
50+
51+
const zipAssets = async (assetsPath: string) => {
52+
const zip = new JSZip();
53+
assembleFiles(assetsPath, zip);
54+
const zipPath = `${assetsPath.replace(/\/$/, '').trim()}.zip`;
55+
await zip
56+
.generateAsync({ type: 'nodebuffer' })
57+
.then((content) => {
58+
fs.writeFileSync(zipPath, content);
59+
logger.info(`Folder compressed to: ${zipPath}`);
60+
})
61+
.catch((e) => {
62+
logger.error(`Failed to compress folder: ${e}`);
63+
throw e;
64+
});
65+
return zipPath;
66+
};
67+
68+
const constructAssets = async ({ files, rootPath }: CdkAssets, region: string) => {
69+
const assets = await Promise.all(
70+
Object.entries(files)
71+
.filter(([, fileItem]) => !fileItem.source.path.endsWith('.template.json'))
72+
.map(async ([, fileItem]) => {
73+
let sourcePath = `${rootPath}/${fileItem.source.path}`;
74+
if (fileItem.source.packaging === 'zip') {
75+
sourcePath = await zipAssets(`${rootPath}/${fileItem.source.path}`);
76+
}
77+
return {
78+
bucketName: get(
79+
fileItem,
80+
'destinations.current_account-current_region.bucketName',
81+
'',
82+
).replace('${ALIYUN::Region}', region),
83+
source: sourcePath,
84+
objectKey: get(fileItem, 'destinations.current_account-current_region.objectKey'),
85+
};
86+
}),
87+
);
88+
89+
return !isEmpty(assets) ? assets : undefined;
90+
};
91+
92+
const ensureBucketExits = async (bucketName: string, ossClient: OSS) =>
93+
await ossClient.getBucketInfo(bucketName).catch((err) => {
94+
if (err.code === 'NoSuchBucket') {
95+
logger.info(`Bucket: ${bucketName} not exists, creating...`);
96+
return ossClient.putBucket(bucketName, {
97+
storageClass: 'Standard',
98+
acl: 'private',
99+
dataRedundancyType: 'LRS',
100+
} as OSS.PutBucketOptions);
101+
} else {
102+
throw err;
103+
}
104+
});
105+
106+
export const publishAssets = async (assets: CdkAssets, context: ActionContext) => {
107+
const constructedAssets = await constructAssets(assets, context.region);
108+
109+
if (!constructedAssets?.length) {
110+
logger.info('No assets to publish, skipped!');
111+
return;
112+
}
113+
114+
const bucketName = constructedAssets[0].bucketName;
115+
116+
const client = new OSS({
117+
region: `oss-${context.region}`,
118+
accessKeyId: context.accessKeyId,
119+
accessKeySecret: context.accessKeySecret,
120+
bucket: bucketName,
121+
});
122+
123+
await ensureBucketExits(bucketName, client);
124+
125+
const headers = {
126+
'x-oss-storage-class': 'Standard',
127+
'x-oss-object-acl': 'private',
128+
'x-oss-forbid-overwrite': 'false',
129+
} as OSS.PutObjectOptions;
130+
131+
await Promise.all(
132+
constructedAssets.map(async ({ source, objectKey }) => {
133+
await client.put(objectKey, path.normalize(source), { headers });
134+
logger.info(`Upload file: ${source}) to bucket: ${bucketName} successfully!`);
135+
}),
136+
);
137+
138+
return bucketName;
139+
};

src/common/rosClient.ts

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ import ROS20190910, {
1010
UpdateStackRequestParameters,
1111
} from '@alicloud/ros20190910';
1212
import { Config } from '@alicloud/openapi-client';
13-
import OSS from 'ali-oss';
14-
import { ActionContext, CdkAssets } from '../types';
13+
import { ActionContext } from '../types';
1514
import { logger } from './logger';
1615
import { lang } from '../lang';
17-
import path from 'node:path';
18-
import { get, isEmpty } from 'lodash';
1916

2017
const client = new ROS20190910(
2118
new Config({
@@ -199,68 +196,3 @@ export const rosStackDelete = async ({
199196
throw new Error(JSON.stringify(err));
200197
}
201198
};
202-
203-
const ensureBucketExits = async (bucketName: string, ossClient: OSS) =>
204-
await ossClient.getBucketInfo(bucketName).catch((err) => {
205-
if (err.code === 'NoSuchBucket') {
206-
logger.info(`Bucket: ${bucketName} not exists, creating...`);
207-
return ossClient.putBucket(bucketName, {
208-
storageClass: 'Standard',
209-
acl: 'private',
210-
dataRedundancyType: 'LRS',
211-
} as OSS.PutBucketOptions);
212-
} else {
213-
throw err;
214-
}
215-
});
216-
217-
const getZipAssets = ({ files, rootPath }: CdkAssets, region: string) => {
218-
const zipAssets = Object.entries(files)
219-
.filter(([, fileItem]) => fileItem.source.path.endsWith('zip'))
220-
.map(([, fileItem]) => ({
221-
bucketName: get(
222-
fileItem,
223-
'destinations.current_account-current_region.bucketName',
224-
'',
225-
).replace('${ALIYUN::Region}', region),
226-
source: `${rootPath}/${fileItem.source.path}`,
227-
objectKey: get(fileItem, 'destinations.current_account-current_region.objectKey'),
228-
}));
229-
230-
return !isEmpty(zipAssets) ? zipAssets : undefined;
231-
};
232-
233-
export const publishAssets = async (assets: CdkAssets, context: ActionContext) => {
234-
const zipAssets = getZipAssets(assets, context.region);
235-
236-
if (!zipAssets) {
237-
logger.info('No assets to publish, skipped!');
238-
return;
239-
}
240-
241-
const bucketName = zipAssets[0].bucketName;
242-
243-
const client = new OSS({
244-
region: `oss-${context.region}`,
245-
accessKeyId: context.accessKeyId,
246-
accessKeySecret: context.accessKeySecret,
247-
bucket: bucketName,
248-
});
249-
250-
await ensureBucketExits(bucketName, client);
251-
252-
const headers = {
253-
'x-oss-storage-class': 'Standard',
254-
'x-oss-object-acl': 'private',
255-
'x-oss-forbid-overwrite': 'false',
256-
} as OSS.PutObjectOptions;
257-
258-
await Promise.all(
259-
zipAssets.map(async ({ source, objectKey }) => {
260-
await client.put(objectKey, path.normalize(source), { headers });
261-
logger.info(`Upload file: ${source}) to bucket: ${bucketName} successfully!`);
262-
}),
263-
);
264-
265-
return bucketName;
266-
};

src/parser/bucketParser.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { BucketDomain, BucketRaw } from '../types';
2+
3+
export const parseBucket = (buckets: {
4+
[key: string]: BucketRaw;
5+
}): Array<BucketDomain> | undefined => {
6+
if (!buckets) {
7+
return undefined;
8+
}
9+
return Object.entries(buckets).map(([key, bucket]) => ({
10+
key,
11+
name: bucket.name,
12+
storage: bucket.storage,
13+
versioning: bucket.versioning,
14+
security: bucket.security
15+
? {
16+
force_delete: bucket.security.force_delete ?? false,
17+
sse_algorithm: bucket.security.sse_algorithm,
18+
sse_kms_master_key_id: bucket.security.sse_kms_master_key_id,
19+
}
20+
: undefined,
21+
22+
website: bucket.website
23+
? {
24+
code: bucket.website.code,
25+
index: bucket.website.index ?? 'index.html',
26+
error_page: bucket.website.error_page ?? '404.html',
27+
error_code: bucket.website.error_code ?? 404,
28+
}
29+
: undefined,
30+
}));
31+
};

src/parser/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseDatabase } from './databaseParser';
66
import { parseTag } from './tagParser';
77
import { parse } from 'yaml';
88
import { validateYaml } from '../validator';
9+
import { parseBucket } from './bucketParser';
910

1011
const validateExistence = (path: string) => {
1112
if (!existsSync(path)) {
@@ -24,6 +25,7 @@ const transformYaml = (iacJson: ServerlessIacRaw): ServerlessIac => {
2425
events: parseEvent(iacJson.events),
2526
databases: parseDatabase(iacJson.databases),
2627
tags: parseTag(iacJson.tags),
28+
buckets: parseBucket(iacJson.buckets),
2729
};
2830
};
2931

src/stack/rosStack/bucket.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ActionContext, BucketDomain } from '../../types';
2+
import * as oss from '@alicloud/ros-cdk-oss';
3+
import * as ros from '@alicloud/ros-cdk-core';
4+
import { getAssets, replaceReference } from '../../common';
5+
import * as ossDeployment from '@alicloud/ros-cdk-ossdeployment';
6+
import path from 'node:path';
7+
8+
export const resolveBuckets = (
9+
scope: ros.Construct,
10+
buckets: Array<BucketDomain> | undefined,
11+
context: ActionContext,
12+
) => {
13+
if (!buckets) {
14+
return undefined;
15+
}
16+
buckets.forEach((bucket) => {
17+
const ossBucket = new oss.Bucket(scope, replaceReference(bucket.key, context), {
18+
bucketName: replaceReference(bucket.name, context),
19+
websiteConfigurationV2: bucket.website
20+
? {
21+
indexDocument: {
22+
type: '0',
23+
suffix: replaceReference(bucket.website.index, context),
24+
supportSubDir: 'true',
25+
},
26+
errorDocument: {
27+
httpStatus: `${replaceReference(bucket.website.error_code, context)}`,
28+
key: replaceReference(bucket.website.error_page, context),
29+
},
30+
}
31+
: undefined,
32+
});
33+
if (bucket.website?.code) {
34+
const filePath = path.resolve(process.cwd(), replaceReference(bucket.website.code, context));
35+
new ossDeployment.BucketDeployment(
36+
scope,
37+
`${replaceReference(bucket.key, context)}_bucket_code_deployment`,
38+
{
39+
sources: getAssets(filePath),
40+
destinationBucket: ossBucket.attrName,
41+
timeout: 3000,
42+
logMonitoring: false,
43+
retainOnCreate: false,
44+
},
45+
true,
46+
);
47+
}
48+
});
49+
};

src/stack/rosStack/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { resolveStages } from './stage';
77
import { resloveVars } from './vars';
88
import { resolveDatabases } from './database';
99
import { resolveEvents } from './event';
10+
import { resolveBuckets } from './bucket';
1011

1112
export * from './bootstrap';
1213

@@ -32,5 +33,7 @@ export class RosStack extends ros.Stack {
3233
resolveEvents(this, iac.events, iac.tags, context, this.service);
3334
// Define Databases
3435
resolveDatabases(this, iac.databases, context);
36+
// Define Buckets
37+
resolveBuckets(this, iac.buckets, context);
3538
}
3639
}

0 commit comments

Comments
 (0)