diff --git a/samples/aliyun-poc-domain.yml b/samples/aliyun-poc-domain.yml new file mode 100644 index 0000000..17d7f80 --- /dev/null +++ b/samples/aliyun-poc-domain.yml @@ -0,0 +1,21 @@ +version: 0.0.1 + +provider: + name: aliyun + region: cn-hongkong + + +service: insight-poc-domain + +tags: + owner: geek-fun + +buckets: + insight_poc_bucket: + name: insight-poc-domain + website: + code: dist + domain: meke-ui.serverlessinsight.com + index: index.html + error_page: 404.html + error_code: 404 diff --git a/samples/aliyun-poc-fc-gpu.yml b/samples/aliyun-poc-fc-gpu.yml index ba53ee8..5f7184b 100644 --- a/samples/aliyun-poc-fc-gpu.yml +++ b/samples/aliyun-poc-fc-gpu.yml @@ -13,7 +13,7 @@ stages: dev: node_env: development prod: - region: cn-shanghai + region: cn-hangzhou service: insight-poc-gpu @@ -28,6 +28,7 @@ functions: cmd: "npm start" port: 9000 memory: 512 + gpu: TESLA_8 timeout: 10 network: vpc_id: vpc-2vc8v9btc8470laqui9bk diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 468bc0e..138285a 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,16 +1,26 @@ import { deployStack } from '../stack'; -import { constructActionContext, logger } from '../common'; +import { constructActionContext, getIacLocation, logger } from '../common'; import { parseYaml } from '../parser'; export const deploy = async ( stackName: string, - options: { location: string; parameters: { [key: string]: string }; stage: string | undefined }, + options: { + location: string; + parameters?: { [key: string]: string }; + stage?: string; + region?: string; + provider?: string; + accessKeyId?: string; + accessKeySecret?: string; + securityToken?: string; + }, ) => { - const context = constructActionContext({ ...options, stackName }); logger.info('Validating yaml...'); - const iac = parseYaml(context.iacLocation); + const iac = parseYaml(getIacLocation(options.location)); logger.info('Yaml is valid! 🎉'); + const context = constructActionContext({ ...options, stackName, iacProvider: iac.provider }); + logger.info('Deploying stack...'); await deployStack(stackName, iac, context); diff --git a/src/commands/index.ts b/src/commands/index.ts index 1d89703..7d1b420 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -28,7 +28,7 @@ program .option('-s, --stage ', 'specify the stage') .action((stackName, { file, stage }) => { logger.debug('log command info'); - validate(file, stage); + validate(stackName, { stage, location: file }); }); program @@ -36,6 +36,11 @@ program .description('deploy serverless Iac yaml') .option('-f, --file ', 'specify the yaml file') .option('-s, --stage ', 'specify the stage') + .option('-r, --region ', 'specify the region') + .option('-pr, --provider ', 'specify the provider') + .option('-ak, --accessKeyId ', 'specify the AccessKeyId') + .option('-as, --accessKeySecret ', 'specify the AccessKeySecret') + .option('-at, --securityToken ', 'specify the SecurityToken') .option( '-p, --parameter ', 'override parameters', @@ -46,9 +51,23 @@ program }, {}, ) - .action(async (stackName, { file, parameter, stage }) => { - await deploy(stackName, { location: file, parameters: parameter, stage }); - }); + .action( + async ( + stackName, + { stage, parameter, file, region, provider, accessKeyId, accessKeySecret, securityToken }, + ) => { + await deploy(stackName, { + stage, + parameters: parameter, + location: file, + region, + provider, + accessKeyId, + accessKeySecret, + securityToken, + }); + }, + ); program .command('template ') diff --git a/src/commands/validate.ts b/src/commands/validate.ts index ce33203..934590c 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -1,8 +1,11 @@ import { constructActionContext, logger } from '../common'; import { parseYaml } from '../parser'; -export const validate = (location: string | undefined, stage: string | undefined) => { - const context = constructActionContext({ location, stage }); +export const validate = ( + stackName: string, + options: { location: string | undefined; stage: string | undefined }, +) => { + const context = constructActionContext({ stackName, ...options }); parseYaml(context.iacLocation); logger.info('Yaml is valid! 🎉'); }; diff --git a/src/common/actionContext.ts b/src/common/actionContext.ts index 395de1a..e9d3795 100644 --- a/src/common/actionContext.ts +++ b/src/common/actionContext.ts @@ -1,4 +1,4 @@ -import { ActionContext } from '../types'; +import { ActionContext, ServerlessIac } from '../types'; import path from 'node:path'; import { ProviderEnum } from './providerEnum'; @@ -12,28 +12,32 @@ export const getIacLocation = (location?: string): string => { path.resolve(projectRoot, 'serverless-insight.yml'); }; -export const constructActionContext = (config?: { +export const constructActionContext = (config: { + stage?: string; + stackName?: string; region?: string; provider?: string; - account?: string; accessKeyId?: string; accessKeySecret?: string; securityToken?: string; location?: string; parameters?: { [key: string]: string }; - stage?: string; - stackName?: string; + iacProvider?: ServerlessIac['provider']; }): ActionContext => { return { - stage: config?.stage ?? 'default', - stackName: config?.stackName ?? '', + stage: config.stage ?? 'default', + stackName: config.stackName ?? '', + provider: (config.provider ?? config.iacProvider?.name ?? ProviderEnum.ALIYUN) as ProviderEnum, region: - config?.region ?? process.env.ROS_REGION_ID ?? process.env.ALIYUN_REGION ?? 'cn-hangzhou', - accessKeyId: config?.accessKeyId ?? (process.env.ALIYUN_ACCESS_KEY_ID as string), - accessKeySecret: config?.accessKeySecret ?? (process.env.ALIYUN_ACCESS_KEY_SECRET as string), - securityToken: config?.securityToken ?? process.env.ALIYUN_SECURITY_TOKEN, - iacLocation: getIacLocation(config?.location), - parameters: Object.entries(config?.parameters ?? {}).map(([key, value]) => ({ key, value })), - provider: config?.provider as ProviderEnum, + config.region ?? + config.iacProvider?.region ?? + process.env.ROS_REGION_ID ?? + process.env.ALIYUN_REGION ?? + 'cn-hangzhou', + accessKeyId: config.accessKeyId ?? (process.env.ALIYUN_ACCESS_KEY_ID as string), + accessKeySecret: config.accessKeySecret ?? (process.env.ALIYUN_ACCESS_KEY_SECRET as string), + securityToken: config.securityToken ?? process.env.ALIYUN_SECURITY_TOKEN, + iacLocation: getIacLocation(config.location), + parameters: Object.entries(config.parameters ?? {}).map(([key, value]) => ({ key, value })), }; }; diff --git a/src/parser/functionParser.ts b/src/parser/functionParser.ts index c23014a..c0a4496 100644 --- a/src/parser/functionParser.ts +++ b/src/parser/functionParser.ts @@ -1,4 +1,4 @@ -import { FunctionDomain, FunctionRaw, NasStorageClassEnum } from '../types'; +import { FunctionDomain, FunctionGpuEnum, FunctionRaw, NasStorageClassEnum } from '../types'; import { isEmpty } from 'lodash'; export const parseFunction = (functions?: { @@ -13,6 +13,7 @@ export const parseFunction = (functions?: { code: func.code, container: func.container, memory: func.memory, + gpu: func.gpu as FunctionGpuEnum, timeout: func.timeout, environment: func.environment, log: func.log, diff --git a/src/stack/rosStack/function.ts b/src/stack/rosStack/function.ts index b668fc0..ac78371 100644 --- a/src/stack/rosStack/function.ts +++ b/src/stack/rosStack/function.ts @@ -1,4 +1,10 @@ -import { ActionContext, FunctionDomain, NasStorageClassEnum, ServerlessIac } from '../../types'; +import { + ActionContext, + FunctionDomain, + FunctionGpuEnum, + NasStorageClassEnum, + ServerlessIac, +} from '../../types'; import { CODE_ZIP_SIZE_LIMIT, encodeBase64ForRosId, @@ -25,6 +31,7 @@ const storageClassMap = { [NasStorageClassEnum.EXTREME_STANDARD]: { fileSystemType: 'extreme', storageType: 'standard' }, [NasStorageClassEnum.EXTREME_ADVANCE]: { fileSystemType: 'extreme', storageType: 'advance' }, }; + const securityGroupRangeMap: { [key: string]: string } = { TCP: '1/65535', UDP: '1/65535', @@ -32,6 +39,17 @@ const securityGroupRangeMap: { [key: string]: string } = { GRE: '-1/-1', ALL: '-1/-1', }; +const gpuConfigMap = { + [FunctionGpuEnum.TESLA_8]: { gpuMemorySize: 8192, gpuType: 'fc.gpu.tesla.1' }, + [FunctionGpuEnum.TESLA_12]: { gpuMemorySize: 12288, gpuType: 'fc.gpu.tesla.1' }, + [FunctionGpuEnum.TESLA_16]: { gpuMemorySize: 16384, gpuType: 'fc.gpu.tesla.1' }, + [FunctionGpuEnum.AMPERE_8]: { gpuMemorySize: 8192, gpuType: 'fc.gpu.ampere.1' }, + [FunctionGpuEnum.AMPERE_12]: { gpuMemorySize: 12288, gpuType: 'fc.gpu.ampere.1' }, + [FunctionGpuEnum.AMPERE_16]: { gpuMemorySize: 16384, gpuType: 'fc.gpu.ampere.1' }, + [FunctionGpuEnum.AMPERE_24]: { gpuMemorySize: 24576, gpuType: 'fc.gpu.ampere.1' }, + [FunctionGpuEnum.ADA_48]: { gpuMemorySize: 49152, gpuType: 'fc.gpu.ada.1' }, +}; + const transformSecurityRules = (rules: Array, ruleType: 'INGRESS' | 'EGRESS') => { return rules.map((rule) => { const [protocol, cidrIp, portRange] = rule.split(':'); @@ -49,6 +67,13 @@ const transformSecurityRules = (rules: Array, ruleType: 'INGRESS' | 'EGR }); }; +const transformGpuConfig = (gpu: FunctionDomain['gpu']) => { + if (!gpu) { + return undefined; + } + + return gpuConfigMap[gpu]; +}; export const resolveFunctions = ( scope: ros.Construct, functions: Array | undefined, @@ -248,8 +273,9 @@ export const resolveFunctions = ( { functionName: replaceReference(fnc.name, context), memorySize: replaceReference(fnc.memory, context), - timeout: replaceReference(fnc.timeout, context), diskSize: fnc.storage?.disk, + gpuConfig: transformGpuConfig(fnc.gpu), + timeout: replaceReference(fnc.timeout, context), environmentVariables: replaceReference(fnc.environment, context), logConfig, vpcConfig, diff --git a/src/types/domains/function.ts b/src/types/domains/function.ts index 01fcff7..396132a 100644 --- a/src/types/domains/function.ts +++ b/src/types/domains/function.ts @@ -11,6 +11,7 @@ export type FunctionRaw = { port: number; }; memory: number; + gpu: string; timeout: number; log?: boolean; environment?: { @@ -48,6 +49,7 @@ export type FunctionDomain = { port: number; }; memory: number; + gpu?: FunctionGpuEnum; timeout: number; log?: boolean; environment?: { @@ -77,3 +79,14 @@ export enum NasStorageClassEnum { EXTREME_STANDARD = 'EXTREME_STANDARD', EXTREME_ADVANCE = 'EXTREME_ADVANCE', } + +export enum FunctionGpuEnum { + TESLA_8 = 'TESLA_8', + TESLA_12 = 'TESLA_12', + TESLA_16 = 'TESLA_16', + AMPERE_8 = 'AMPERE_8', + AMPERE_12 = 'AMPERE_12', + AMPERE_16 = 'AMPERE_16', + AMPERE_24 = 'AMPERE_24', + ADA_48 = 'ADA_48', +} diff --git a/src/validator/functionSchema.ts b/src/validator/functionSchema.ts index c762ba2..7b53daa 100644 --- a/src/validator/functionSchema.ts +++ b/src/validator/functionSchema.ts @@ -46,6 +46,19 @@ export const functionSchema = { }, }, memory: { type: 'number' }, + gpu: { + type: 'string', + enum: [ + 'TESLA_8', + 'TESLA_12', + 'TESLA_16', + 'AMPERE_8', + 'AMPERE_12', + 'AMPERE_16', + 'AMPERE_24', + 'ADA_48', + ], + }, timeout: { type: 'number' }, log: { type: 'boolean' }, environment: { diff --git a/tests/commands/deploy.test.ts b/tests/commands/deploy.test.ts index 8b03db1..cb8d59e 100644 --- a/tests/commands/deploy.test.ts +++ b/tests/commands/deploy.test.ts @@ -18,6 +18,9 @@ describe('unit test for deploy command', () => { }); expect(mockedDeployStack).toHaveBeenCalledTimes(1); - expect(mockedDeployStack).toHaveBeenCalledWith(stackName, expect.any(Object), defaultContext); + expect(mockedDeployStack).toHaveBeenCalledWith(stackName, expect.any(Object), { + ...defaultContext, + region: 'cn-chengdu', + }); }); }); diff --git a/tests/fixtures/deployFixture.ts b/tests/fixtures/deployFixture.ts index 4760d82..6b67326 100644 --- a/tests/fixtures/deployFixture.ts +++ b/tests/fixtures/deployFixture.ts @@ -994,6 +994,32 @@ export const oneFcWithContainerRos = { }, }; +export const oneFcWithGpuIac = { + ...oneFcIac, + functions: [ + { + ...((oneFcIac.functions && oneFcIac.functions[0]) ?? {}), + gpu: 'TESLA_8', + }, + ], +} as ServerlessIac; +export const oneFcWithGpuRos = { + ...oneFcRos, + Resources: { + ...oneFcRos.Resources, + hello_fn: { + ...oneFcRos.Resources.hello_fn, + Properties: { + ...oneFcRos.Resources.hello_fn.Properties, + GpuConfig: { + GpuMemorySize: 8192, + GpuType: 'fc.gpu.tesla.1', + }, + }, + }, + }, +}; + export const largeCodeRos = { Description: 'my-demo-service stack', Mappings: { @@ -1381,6 +1407,7 @@ export const defaultContext = { iacLocation: expect.stringContaining('tests/fixtures/serverless-insight.yml'), parameters: [], region: 'cn-hangzhou', + provider: 'aliyun', securityToken: 'account id', stackName: 'my-demo-stack', stage: 'default', diff --git a/tests/stack/deploy.test.ts b/tests/stack/deploy.test.ts index e201112..4377885 100644 --- a/tests/stack/deploy.test.ts +++ b/tests/stack/deploy.test.ts @@ -19,6 +19,8 @@ import { oneFcRos, oneFcWithContainerIac, oneFcWithContainerRos, + oneFcWithGpuIac, + oneFcWithGpuRos, oneFcWithStageRos, referredServiceIac, referredServiceRos, @@ -217,7 +219,7 @@ describe('Unit tests for stack deployment', () => { }); }); - describe('unit test for function nas attachment', () => { + describe('unit test for serverless Gpu', () => { it('should deploy function with nas when nas field is provided', async () => { const stackName = 'my-demo-stack-with-nas'; mockedRosStackDeploy.mockResolvedValue(stackName); @@ -231,9 +233,7 @@ describe('Unit tests for stack deployment', () => { { stackName }, ]); }); - }); - describe('unit test for function with container', () => { it('should deploy function with container when container field is provided', async () => { const stackName = 'my-demo-stack-with-container'; mockedRosStackDeploy.mockResolvedValue(stackName); @@ -247,5 +247,19 @@ describe('Unit tests for stack deployment', () => { { stackName }, ]); }); + + it('should deploy function with gpu configured', async () => { + const stackName = 'my-demo-stack-with-gpu'; + mockedRosStackDeploy.mockResolvedValue(stackName); + + await deployStack(stackName, oneFcWithGpuIac, { stackName } as ActionContext); + + expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ + stackName, + oneFcWithGpuRos, + { stackName }, + ]); + }); }); });