diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 138285a..7954728 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,5 +1,5 @@ import { deployStack } from '../stack'; -import { constructActionContext, getIacLocation, logger } from '../common'; +import { getIacLocation, logger, setContext } from '../common'; import { parseYaml } from '../parser'; export const deploy = async ( @@ -19,10 +19,10 @@ export const deploy = async ( const iac = parseYaml(getIacLocation(options.location)); logger.info('Yaml is valid! 🎉'); - const context = constructActionContext({ ...options, stackName, iacProvider: iac.provider }); + setContext({ ...options, stackName, iacProvider: iac.provider }); logger.info('Deploying stack...'); - await deployStack(stackName, iac, context); + await deployStack(stackName, iac); logger.info('Stack deployed! 🎉'); }; diff --git a/src/commands/destroy.ts b/src/commands/destroy.ts index d355425..529a1cd 100644 --- a/src/commands/destroy.ts +++ b/src/commands/destroy.ts @@ -1,4 +1,4 @@ -import { constructActionContext, getIacLocation, logger, rosStackDelete } from '../common'; +import { getContext, getIacLocation, logger, rosStackDelete, setContext } from '../common'; import { parseYaml } from '../parser'; export const destroyStack = async ( @@ -13,7 +13,8 @@ export const destroyStack = async ( }, ) => { const iac = parseYaml(getIacLocation(options.location)); - const context = constructActionContext({ stackName, ...options, iacProvider: iac.provider }); + setContext({ stackName, ...options, iacProvider: iac.provider }); + const context = getContext(); logger.info( `Destroying stack: ${stackName}, provider: ${context.provider}, region: ${context.region}...`, ); diff --git a/src/commands/index.ts b/src/commands/index.ts index 8cbad7b..b8e605d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,7 +1,7 @@ #! /usr/bin/env node import { Command } from 'commander'; -import { constructActionContext, getVersion, logger } from '../common'; +import { getContext, getVersion, logger, setContext } from '../common'; import { validate } from './validate'; import { deploy } from './deploy'; import { template } from './template'; @@ -16,7 +16,8 @@ program .command('show') .description('show string') .action(async (options) => { - const context = constructActionContext({ ...options }); + setContext({ ...options }); + const context = getContext(); const result = await getIamInfo(context); console.log('result:', JSON.stringify(result)); }); diff --git a/src/commands/template.ts b/src/commands/template.ts index fbbe51f..16adcd1 100644 --- a/src/commands/template.ts +++ b/src/commands/template.ts @@ -1,7 +1,7 @@ import { TemplateFormat } from '../types'; import yaml from 'yaml'; import { generateStackTemplate } from '../stack/deploy'; -import { constructActionContext, getIacLocation, logger } from '../common'; +import { getIacLocation, logger, setContext } from '../common'; import { parseYaml } from '../parser'; export const template = ( @@ -10,9 +10,9 @@ export const template = ( ) => { const iac = parseYaml(getIacLocation(options.location)); - const context = constructActionContext({ ...options, stackName, provider: iac.provider.name }); + setContext({ ...options, stackName, provider: iac.provider.name }); - const { template } = generateStackTemplate(stackName, iac, context); + const { template } = generateStackTemplate(stackName, iac); if (typeof template === 'string') { logger.info(`\n${template}`); } else { diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 934590c..00c56da 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -1,11 +1,12 @@ -import { constructActionContext, logger } from '../common'; +import { getContext, logger, setContext } from '../common'; import { parseYaml } from '../parser'; export const validate = ( stackName: string, options: { location: string | undefined; stage: string | undefined }, ) => { - const context = constructActionContext({ stackName, ...options }); + setContext({ stackName, ...options }); + const context = getContext(); parseYaml(context.iacLocation); logger.info('Yaml is valid! 🎉'); }; diff --git a/src/common/actionContext.ts b/src/common/context.ts similarity index 76% rename from src/common/actionContext.ts rename to src/common/context.ts index e9d3795..1ece23f 100644 --- a/src/common/actionContext.ts +++ b/src/common/context.ts @@ -1,6 +1,9 @@ -import { ActionContext, ServerlessIac } from '../types'; +import { Context, ServerlessIac } from '../types'; import path from 'node:path'; import { ProviderEnum } from './providerEnum'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +const asyncLocalStorage = new AsyncLocalStorage(); export const getIacLocation = (location?: string): string => { const projectRoot = path.resolve(process.cwd()); @@ -12,7 +15,7 @@ export const getIacLocation = (location?: string): string => { path.resolve(projectRoot, 'serverless-insight.yml'); }; -export const constructActionContext = (config: { +export const setContext = (config: { stage?: string; stackName?: string; region?: string; @@ -23,8 +26,8 @@ export const constructActionContext = (config: { location?: string; parameters?: { [key: string]: string }; iacProvider?: ServerlessIac['provider']; -}): ActionContext => { - return { +}): void => { + const context = { stage: config.stage ?? 'default', stackName: config.stackName ?? '', provider: (config.provider ?? config.iacProvider?.name ?? ProviderEnum.ALIYUN) as ProviderEnum, @@ -40,4 +43,14 @@ export const constructActionContext = (config: { iacLocation: getIacLocation(config.location), parameters: Object.entries(config.parameters ?? {}).map(([key, value]) => ({ key, value })), }; + + asyncLocalStorage.enterWith(context); +}; + +export const getContext = (): Context => { + const context = asyncLocalStorage.getStore(); + if (!context) { + throw new Error('No context found'); + } + return context; }; diff --git a/src/common/iacHelper.ts b/src/common/iacHelper.ts index ba3c6f7..675995c 100644 --- a/src/common/iacHelper.ts +++ b/src/common/iacHelper.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import fs from 'node:fs'; import * as ros from '@alicloud/ros-cdk-core'; -import { ActionContext } from '../types'; +import { Context } from '../types'; import * as ossDeployment from '@alicloud/ros-cdk-ossdeployment'; import crypto from 'node:crypto'; @@ -34,13 +34,13 @@ export const getFileSource = ( return { source, objectKey }; }; -const evalCtx = (value: string, ctx: ActionContext): string => { +const evalCtx = (value: string, ctx: Context): string => { const containsStage = value.match(/\$\{ctx.\w+}/); return containsStage ? value.replace(/\$\{ctx.stage}/g, ctx.stage) : value; }; -export const replaceReference = (value: T, ctx: ActionContext): T => { +export const replaceReference = (value: T, ctx: Context): T => { if (typeof value === 'string') { const matchVar = value.match(/^\$\{vars\.(\w+)}$/); const containsVar = value.match(/\$\{vars\.(\w+)}/); diff --git a/src/common/imsClient.ts b/src/common/imsClient.ts index f0aa18e..69413ce 100644 --- a/src/common/imsClient.ts +++ b/src/common/imsClient.ts @@ -1,8 +1,8 @@ import Ims20190815, * as ims20190815 from '@alicloud/ims20190815'; import * as openApi from '@alicloud/openapi-client'; -import { ActionContext } from '../types'; +import { Context } from '../types'; -export const getIamInfo = async (context: ActionContext) => { +export const getIamInfo = async (context: Context) => { const imsClient = new Ims20190815( new openApi.Config({ accessKeyId: context.accessKeyId, diff --git a/src/common/index.ts b/src/common/index.ts index af118b8..c153197 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -2,7 +2,7 @@ export * from './providerEnum'; export * from './logger'; export * from './getVersion'; export * from './rosClient'; -export * from './actionContext'; +export * from './context'; export * from './iacHelper'; export * from './constants'; export * from './imsClient'; diff --git a/src/common/rosAssets.ts b/src/common/rosAssets.ts index 8e77e83..226ee9c 100644 --- a/src/common/rosAssets.ts +++ b/src/common/rosAssets.ts @@ -4,9 +4,10 @@ import * as ossDeployment from '@alicloud/ros-cdk-ossdeployment'; import path from 'node:path'; import JSZip from 'jszip'; import { logger } from './logger'; -import { ActionContext, CdkAssets } from '../types'; +import { CdkAssets } from '../types'; import { get, isEmpty } from 'lodash'; import OSS from 'ali-oss'; +import { getContext } from './context'; const buildAssets = (rootPath: string, relativePath: string): Array => { const location = path.resolve(rootPath, relativePath); @@ -71,10 +72,11 @@ type ConstructedAsset = { objectKey: string; }; -export const constructAssets = async ( - { files, rootPath }: CdkAssets, - region: string, -): Promise | undefined> => { +export const constructAssets = async ({ + files, + rootPath, +}: CdkAssets): Promise | undefined> => { + const { region } = getContext(); const assets = await Promise.all( Object.entries(files) .filter(([, fileItem]) => !fileItem.source.path.endsWith('.template.json')) @@ -112,15 +114,13 @@ const ensureBucketExits = async (bucketName: string, ossClient: OSS) => } }); -export const publishAssets = async ( - assets: Array | undefined, - context: ActionContext, -) => { +export const publishAssets = async (assets: Array | undefined) => { if (!assets?.length) { logger.info('No assets to publish, skipped!'); return; } + const context = getContext(); const bucketName = assets[0].bucketName; const client = new OSS({ @@ -148,15 +148,12 @@ export const publishAssets = async ( return bucketName; }; -export const cleanupAssets = async ( - assets: Array | undefined, - context: ActionContext, -) => { +export const cleanupAssets = async (assets: Array | undefined) => { if (!assets?.length) { logger.info('No assets to cleanup, skipped!'); return; } - + const context = getContext(); const bucketName = assets[0].bucketName; const client = new OSS({ diff --git a/src/common/rosClient.ts b/src/common/rosClient.ts index 4590822..86ce1e6 100644 --- a/src/common/rosClient.ts +++ b/src/common/rosClient.ts @@ -10,9 +10,10 @@ import ROS20190910, { UpdateStackRequestParameters, } from '@alicloud/ros20190910'; import { Config } from '@alicloud/openapi-client'; -import { ActionContext } from '../types'; +import { Context } from '../types'; import { logger } from './logger'; import { lang } from '../lang'; +import { getContext } from './context'; const client = new ROS20190910( new Config({ @@ -23,7 +24,7 @@ const client = new ROS20190910( }), ); -const createStack = async (stackName: string, templateBody: unknown, context: ActionContext) => { +const createStack = async (stackName: string, templateBody: unknown, context: Context) => { const parameters = context.parameters?.map( (parameter) => new CreateStackRequestParameters({ @@ -47,7 +48,7 @@ const createStack = async (stackName: string, templateBody: unknown, context: Ac return await getStackActionResult(response.body?.stackId || '', context.region); }; -const updateStack = async (stackId: string, templateBody: unknown, context: ActionContext) => { +const updateStack = async (stackId: string, templateBody: unknown, context: Context) => { const parameters = context.parameters?.map( (parameter) => new UpdateStackRequestParameters({ @@ -156,11 +157,8 @@ const getStackActionResult = async ( }); }; -export const rosStackDeploy = async ( - stackName: string, - templateBody: unknown, - context: ActionContext, -) => { +export const rosStackDeploy = async (stackName: string, templateBody: unknown) => { + const context = getContext(); const stackInfo = await getStackByName(stackName, context.region); if (stackInfo) { const { Status: stackStatus } = stackInfo; @@ -184,8 +182,9 @@ export const rosStackDeploy = async ( export const rosStackDelete = async ({ stackName, region, -}: Pick) => { +}: Pick) => { const stackInfo = await getStackByName(stackName, region); + if (!stackInfo) { logger.warn(`Stack: ${stackName} not exists, skipped! 🚫`); return; diff --git a/src/stack/deploy.ts b/src/stack/deploy.ts index 87e32e3..2a4033b 100644 --- a/src/stack/deploy.ts +++ b/src/stack/deploy.ts @@ -1,10 +1,11 @@ import * as ros from '@alicloud/ros-cdk-core'; import fs from 'node:fs'; -import { ActionContext, ServerlessIac } from '../types'; +import { ServerlessIac } from '../types'; import { cleanupAssets, constructAssets, + getContext, logger, ProviderEnum, publishAssets, @@ -14,11 +15,8 @@ import { prepareBootstrapStack, RosStack } from './rosStack'; import { RfsStack } from './rfsStack'; import { get } from 'lodash'; -export const generateRosStackTemplate = ( - stackName: string, - iac: ServerlessIac, - context: ActionContext, -) => { +export const generateRosStackTemplate = (stackName: string, iac: ServerlessIac) => { + const context = getContext(); const app = new ros.App(); new RosStack(app, iac, context); @@ -36,12 +34,8 @@ export const generateRosStackTemplate = ( return { template, assets }; }; -export const generateRfsStackTemplate = ( - stackName: string, - iac: ServerlessIac, - context: ActionContext, -) => { - const stack = new RfsStack(iac, context); +export const generateRfsStackTemplate = (stackName: string, iac: ServerlessIac) => { + const stack = new RfsStack(iac); const hcl = stack.toHclTerraform(); console.log('HCL:', hcl); @@ -49,26 +43,22 @@ export const generateRfsStackTemplate = ( return { template: hcl }; }; -export const deployStack = async ( - stackName: string, - iac: ServerlessIac, - context: ActionContext, -) => { - const { template, assets } = generateRosStackTemplate(stackName, iac, context); - await prepareBootstrapStack(context); +export const deployStack = async (stackName: string, iac: ServerlessIac) => { + const { template, assets } = generateRosStackTemplate(stackName, iac); + await prepareBootstrapStack(); logger.info(`Deploying stack, publishing assets...`); - const constructedAssets = await constructAssets(assets, context.region); + const constructedAssets = await constructAssets(assets); try { - await publishAssets(constructedAssets, context); + await publishAssets(constructedAssets); logger.info(`Assets published! 🎉`); - await rosStackDeploy(stackName, template, context); + await rosStackDeploy(stackName, template); } catch (e) { logger.error(`Failed to deploy stack: ${e}`); throw e; } finally { try { logger.info(`Cleaning up temporary Assets...`); - await cleanupAssets(constructedAssets, context); + await cleanupAssets(constructedAssets); logger.info(`Assets cleaned up!♻️`); } catch (e) { logger.error( @@ -81,12 +71,11 @@ export const deployStack = async ( export const generateStackTemplate = ( stackName: string, iac: ServerlessIac, - context: ActionContext, ): { template: unknown } => { if (iac.provider.name === ProviderEnum.ALIYUN) { - return generateRosStackTemplate(stackName, iac, context); + return generateRosStackTemplate(stackName, iac); } else if (iac.provider.name === ProviderEnum.HUAWEI) { - return generateRfsStackTemplate(stackName, iac, context); + return generateRfsStackTemplate(stackName, iac); } return { template: '' }; }; diff --git a/src/stack/rfsStack/function.ts b/src/stack/rfsStack/function.ts index 3629af4..ef802f5 100644 --- a/src/stack/rfsStack/function.ts +++ b/src/stack/rfsStack/function.ts @@ -1,8 +1,8 @@ -import { ActionContext, FunctionDomain } from '../../types'; +import { Context, FunctionDomain } from '../../types'; import { resolveCode } from '../../common'; import { RfsStack } from './index'; -const fgsApplication = (context: ActionContext, service: string) => ` +const fgsApplication = (context: Context, service: string) => ` resource "huaweicloud_fgs_application" "${service}_app" { name = "${service}-app" description = "${service} application" @@ -10,7 +10,7 @@ resource "huaweicloud_fgs_application" "${service}_app" { } `; -const fgsFunction = (fn: FunctionDomain, context: ActionContext, service: string) => ` +const fgsFunction = (fn: FunctionDomain, context: Context, service: string) => ` resource "huaweicloud_fgs_function" "${fn.key}" { name = "${fn.name}" handler = "${fn.code!.handler}" @@ -27,7 +27,7 @@ resource "huaweicloud_fgs_function" "${fn.key}" { export const resolveFunction = ( stack: RfsStack, functions: Array | undefined, - context: ActionContext, + context: Context, service: string, ) => { if (!functions) { diff --git a/src/stack/rfsStack/index.ts b/src/stack/rfsStack/index.ts index 5b637fe..b9ee67f 100644 --- a/src/stack/rfsStack/index.ts +++ b/src/stack/rfsStack/index.ts @@ -1,7 +1,8 @@ -import { ActionContext, ServerlessIac } from '../../types'; +import { Context, ServerlessIac } from '../../types'; import { resolveFunction } from './function'; +import { getContext } from '../../common'; -const provider = (stack: RfsStack, context: ActionContext) => { +const provider = (stack: RfsStack, context: Context) => { const hcl = ` terraform { required_providers { @@ -26,7 +27,7 @@ export class RfsStack { constructor( private readonly iac: ServerlessIac, - private readonly context: ActionContext, + private readonly context = getContext(), ) { provider(this, context); resolveFunction(this, iac.functions, context, iac.service); diff --git a/src/stack/rosStack/bootstrap.ts b/src/stack/rosStack/bootstrap.ts index 465806f..33e5811 100644 --- a/src/stack/rosStack/bootstrap.ts +++ b/src/stack/rosStack/bootstrap.ts @@ -1,7 +1,7 @@ -import { getIamInfo, rosStackDeploy } from '../../common'; -import { ActionContext } from '../../types'; +import { getContext, getIamInfo, rosStackDeploy } from '../../common'; +import { Context } from '../../types'; -const getBootstrapTemplate = async (context: ActionContext) => { +const getBootstrapTemplate = async (context: Context) => { const iamInfo = await getIamInfo(context); const stackName = `serverlessInsight-bootstrap-${iamInfo?.accountId}-${context.region}`; @@ -34,7 +34,8 @@ const getBootstrapTemplate = async (context: ActionContext) => { return { stackName, template }; }; -export const prepareBootstrapStack = async (context: ActionContext) => { +export const prepareBootstrapStack = async () => { + const context = getContext(); const { stackName, template } = await getBootstrapTemplate(context); - await rosStackDeploy(stackName, template, context); + await rosStackDeploy(stackName, template); }; diff --git a/src/stack/rosStack/bucket.ts b/src/stack/rosStack/bucket.ts index e8ce41b..aeb33b8 100644 --- a/src/stack/rosStack/bucket.ts +++ b/src/stack/rosStack/bucket.ts @@ -1,4 +1,4 @@ -import { ActionContext, BucketAccessEnum, BucketDomain } from '../../types'; +import { BucketAccessEnum, BucketDomain, Context } from '../../types'; import * as oss from '@alicloud/ros-cdk-oss'; import * as ros from '@alicloud/ros-cdk-core'; import { @@ -22,7 +22,7 @@ const aclMap = new Map([ export const resolveBuckets = ( scope: ros.Construct, buckets: Array | undefined, - context: ActionContext, + context: Context, ) => { if (!buckets) { return undefined; diff --git a/src/stack/rosStack/database.ts b/src/stack/rosStack/database.ts index 8cb2297..2950eee 100644 --- a/src/stack/rosStack/database.ts +++ b/src/stack/rosStack/database.ts @@ -1,7 +1,7 @@ import * as ros from '@alicloud/ros-cdk-core'; import * as rds from '@alicloud/ros-cdk-rds'; import { replaceReference } from '../../common'; -import { ActionContext, DatabaseDomain, DatabaseEnum, DatabaseVersionEnum } from '../../types'; +import { Context, DatabaseDomain, DatabaseEnum, DatabaseVersionEnum } from '../../types'; import { isEmpty } from 'lodash'; import * as esServerless from '@alicloud/ros-cdk-elasticsearchserverless'; @@ -196,7 +196,7 @@ const rdsEngineMap = new Map< export const resolveDatabases = ( scope: ros.Construct, databases: Array | undefined, - context: ActionContext, + context: Context, ) => { if (isEmpty(databases)) { return undefined; diff --git a/src/stack/rosStack/event.ts b/src/stack/rosStack/event.ts index d95856e..9f80ec1 100644 --- a/src/stack/rosStack/event.ts +++ b/src/stack/rosStack/event.ts @@ -1,5 +1,5 @@ import * as ros from '@alicloud/ros-cdk-core'; -import { ActionContext, EventDomain, EventTypes, ServerlessIac } from '../../types'; +import { Context, EventDomain, EventTypes, ServerlessIac } from '../../types'; import * as ram from '@alicloud/ros-cdk-ram'; import { encodeBase64ForRosId, replaceReference, splitDomain } from '../../common'; import * as agw from '@alicloud/ros-cdk-apigateway'; @@ -10,7 +10,7 @@ export const resolveEvents = ( scope: ros.Construct, events: Array | undefined, tags: ServerlessIac['tags'] | undefined, - context: ActionContext, + context: Context, service: string, ) => { if (isEmpty(events)) { diff --git a/src/stack/rosStack/function.ts b/src/stack/rosStack/function.ts index d4b6bb5..9cca03f 100644 --- a/src/stack/rosStack/function.ts +++ b/src/stack/rosStack/function.ts @@ -1,5 +1,5 @@ import { - ActionContext, + Context, FunctionDomain, FunctionGpuEnum, NasStorageClassEnum, @@ -80,7 +80,7 @@ export const resolveFunctions = ( scope: ros.Construct, functions: Array | undefined, tags: ServerlessIac['tags'] | undefined, - context: ActionContext, + context: Context, service: string, ) => { if (isEmpty(functions)) { diff --git a/src/stack/rosStack/index.ts b/src/stack/rosStack/index.ts index 7c974fb..db13c14 100644 --- a/src/stack/rosStack/index.ts +++ b/src/stack/rosStack/index.ts @@ -1,5 +1,5 @@ import * as ros from '@alicloud/ros-cdk-core'; -import { ActionContext, ServerlessIac } from '../../types'; +import { Context, ServerlessIac } from '../../types'; import { replaceReference } from '../../common'; import { resolveTags } from './tag'; import { resolveFunctions } from './function'; @@ -14,7 +14,7 @@ export * from './bootstrap'; export class RosStack extends ros.Stack { private readonly service: string; - constructor(scope: ros.Construct, iac: ServerlessIac, context: ActionContext) { + constructor(scope: ros.Construct, iac: ServerlessIac, context: Context) { super(scope, replaceReference(iac.service, context), { stackName: context.stackName, tags: resolveTags(iac.tags, context), diff --git a/src/stack/rosStack/stage.ts b/src/stack/rosStack/stage.ts index 7556c35..76478a3 100644 --- a/src/stack/rosStack/stage.ts +++ b/src/stack/rosStack/stage.ts @@ -1,12 +1,12 @@ import * as ros from '@alicloud/ros-cdk-core'; import { replaceReference } from '../../common'; -import { ActionContext, Stages } from '../../types'; +import { Context, Stages } from '../../types'; import { isEmpty } from 'lodash'; export const resolveStages = ( scope: ros.Construct, stages: Stages | undefined, - context: ActionContext, + context: Context, ) => { if (isEmpty(stages)) { return undefined; diff --git a/src/stack/rosStack/tag.ts b/src/stack/rosStack/tag.ts index 8cd887e..156fde7 100644 --- a/src/stack/rosStack/tag.ts +++ b/src/stack/rosStack/tag.ts @@ -1,7 +1,7 @@ -import { ActionContext, ServerlessIac } from '../../types'; +import { Context, ServerlessIac } from '../../types'; import { replaceReference } from '../../common'; -export const resolveTags = (tags: ServerlessIac['tags'], context: ActionContext) => { +export const resolveTags = (tags: ServerlessIac['tags'], context: Context) => { return tags?.reduce((acc: { [key: string]: string }, tag) => { acc[tag.key] = replaceReference(tag.value, context); return acc; diff --git a/src/types/domains/context.ts b/src/types/domains/context.ts index eb19a90..a0ac892 100644 --- a/src/types/domains/context.ts +++ b/src/types/domains/context.ts @@ -1,6 +1,6 @@ import { ProviderEnum } from '../../common'; -export type ActionContext = { +export type Context = { region: string; provider: ProviderEnum; stackName: string; diff --git a/tests/commands/deploy.test.ts b/tests/commands/deploy.test.ts index cb8d59e..ae08a4f 100644 --- a/tests/commands/deploy.test.ts +++ b/tests/commands/deploy.test.ts @@ -1,5 +1,4 @@ import { deploy } from '../../src/commands/deploy'; -import { defaultContext } from '../fixtures/deployFixture'; const mockedDeployStack = jest.fn(); jest.mock('../../src/stack', () => ({ @@ -18,9 +17,6 @@ describe('unit test for deploy command', () => { }); expect(mockedDeployStack).toHaveBeenCalledTimes(1); - expect(mockedDeployStack).toHaveBeenCalledWith(stackName, expect.any(Object), { - ...defaultContext, - region: 'cn-chengdu', - }); + expect(mockedDeployStack).toHaveBeenCalledWith(stackName, expect.any(Object)); }); }); diff --git a/tests/common/rosAssets.test.ts b/tests/common/rosAssets.test.ts index a5bb0eb..890d691 100644 --- a/tests/common/rosAssets.test.ts +++ b/tests/common/rosAssets.test.ts @@ -1,9 +1,8 @@ import { cleanupAssets, constructAssets, getAssets, publishAssets } from '../../src/common'; import { Stats } from 'node:fs'; import { assetsFixture } from '../fixtures/assetsFixture'; -import { ActionContext } from '../../src/types'; -import { defaultContext } from '../fixtures/deployFixture'; +const mockedGetStore = jest.fn(); const mockedBucketPut = jest.fn(); const mockedDeleteBucket = jest.fn(); const mockedDelete = jest.fn(); @@ -14,6 +13,13 @@ const mockedGenerateAsync = jest.fn(); const mockedInfoLogger = jest.fn(); const mockedDebugLogger = jest.fn(); +jest.mock('node:async_hooks', () => ({ + AsyncLocalStorage: jest.fn().mockImplementation(() => ({ + enterWith: jest.fn(), + getStore: (...args: unknown[]) => mockedGetStore(...args), + })), +})); + jest.mock('ali-oss', () => jest.fn().mockImplementation(() => ({ getBucketInfo: jest.fn().mockResolvedValue({}), @@ -52,9 +58,15 @@ jest.mock('../../src/common/logger', () => ({ debug: (...args: unknown[]) => mockedDebugLogger(...args), }, })); - describe('Unit test for rosAssets', () => { beforeEach(() => { + mockedGetStore.mockReturnValue({ + region: 'mock-region', + accessKeyId: 'mock-access-key-id', + accessKeySecret: 'mock-access-key-secret', + }); + }); + afterEach(() => { jest.clearAllMocks(); }); describe('Unit test for getAssets', () => { @@ -86,12 +98,6 @@ describe('Unit test for rosAssets', () => { }); describe('Unit test for publishAssets', () => { - const mockContext = { - region: 'mock-region', - accessKeyId: 'mock-access-key-id', - accessKeySecret: 'mock-access-key-secret', - } as ActionContext; - it('should publish assets to the specified bucket', async () => { mockedExistsSync.mockReturnValue(true); mockedReaddirSync.mockReturnValueOnce(['file1', 'file2']); @@ -100,10 +106,7 @@ describe('Unit test for rosAssets', () => { .mockReturnValueOnce({ isFile: () => true } as Stats); mockedGenerateAsync.mockResolvedValue(Buffer.from('mock-zip-content')); - const bucketName = await publishAssets( - await constructAssets(assetsFixture, 'mock-region'), - mockContext, - ); + const bucketName = await publishAssets(await constructAssets(assetsFixture)); expect(bucketName).toBe('cdk-ajmywduza-assets-mock-region'); expect(mockedBucketPut.mock.calls).toEqual([ @@ -126,7 +129,7 @@ describe('Unit test for rosAssets', () => { }); it('should log and skip if no assets to publish', async () => { - await publishAssets([], mockContext); + await publishAssets([]); expect(mockedInfoLogger).toHaveBeenCalledWith('No assets to publish, skipped!'); }); @@ -141,7 +144,7 @@ describe('Unit test for rosAssets', () => { .mockReturnValueOnce({ isFile: () => true } as Stats); mockedGenerateAsync.mockResolvedValue(Buffer.from('mock-zip-content')); - await cleanupAssets(await constructAssets(assetsFixture, 'mock-region'), defaultContext); + await cleanupAssets(await constructAssets(assetsFixture)); expect(mockedDelete).toHaveBeenCalledTimes(2); expect(mockedDelete.mock.calls).toEqual([ @@ -153,7 +156,7 @@ describe('Unit test for rosAssets', () => { }); it('should skip the cleanupAssets when there is no assets', async () => { - await cleanupAssets([], defaultContext); + await cleanupAssets([]); expect(mockedInfoLogger).toHaveBeenCalledWith('No assets to cleanup, skipped!'); }); diff --git a/tests/common/rosClient.test.ts b/tests/common/rosClient.test.ts index 1bd64d9..b76bbcd 100644 --- a/tests/common/rosClient.test.ts +++ b/tests/common/rosClient.test.ts @@ -2,11 +2,20 @@ import { logger, rosStackDelete, rosStackDeploy } from '../../src/common'; import { context } from '../fixtures/contextFixture'; import { lang } from '../../src/lang'; +const mockedGetStore = jest.fn(); const mockedCreateStack = jest.fn(); const mockedUpdateStack = jest.fn(); const mockedListStacks = jest.fn(); const mockedGetStack = jest.fn(); const mockedDeleteStack = jest.fn(); + +jest.mock('node:async_hooks', () => ({ + AsyncLocalStorage: jest.fn().mockImplementation(() => ({ + enterWith: jest.fn(), + getStore: (...args: unknown[]) => mockedGetStore(...args), + })), +})); + jest.mock('@alicloud/ros20190910', () => ({ ...jest.requireActual('@alicloud/ros20190910'), __esModule: true, @@ -28,17 +37,21 @@ describe('Unit test for rosClient', () => { describe('Unit tes for rosStackDeploy', () => { it('should create a new stack if it does not exist', async () => { + const stackName = 'newStack'; + mockedGetStore.mockReturnValue({ stackName }); mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } }); mockedCreateStack.mockResolvedValue({ body: { stackId: 'newStackId' } }); mockedGetStack.mockResolvedValue({ body: { status: 'CREATE_COMPLETE' } }); - await rosStackDeploy('newStack', {}, context); + await rosStackDeploy(stackName, {}); expect(mockedCreateStack).toHaveBeenCalled(); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('createStack success')); }); it('should update an existing stack if it exists', async () => { + const stackName = 'existingStack'; + mockedGetStore.mockReturnValue({ stackName }); mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] }, @@ -46,44 +59,51 @@ describe('Unit test for rosClient', () => { mockedUpdateStack.mockResolvedValue({ body: { stackId: 'existingStackId' } }); mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } }); - await rosStackDeploy('existingStack', {}, context); + await rosStackDeploy(stackName, {}); expect(mockedUpdateStack).toHaveBeenCalled(); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('stackUpdate success')); }); it('should throw an error if the stack is in progress', async () => { + const stackName = 'inProgressStack'; + mockedGetStore.mockReturnValue({ stackName }); mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [{ stackId: 'inProgressStackId', Status: 'CREATE_IN_PROGRESS' }] }, }); - await expect(rosStackDeploy('inProgressStack', {}, context)).rejects.toThrow( + await expect(rosStackDeploy(stackName, {})).rejects.toThrow( 'fail to update stack, because stack status is CREATE_IN_PROGRESS', ); }); it('should notify user with warning logs when update completely same stack', async () => { + const stackName = 'existingStack'; + mockedGetStore.mockReturnValue({ stackName }); mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [{ stackId: 'existingStackId', Status: 'CREATE_COMPLETE' }] }, }); mockedUpdateStack.mockRejectedValueOnce({ - data: { statusCode: 400, Message: 'Update the completely same stack' }, + statusCode: 400, + message: 'Update the completely same stack', }); mockedGetStack.mockResolvedValue({ body: { status: 'UPDATE_COMPLETE' } }); - await rosStackDeploy('existingStack', {}, context); + await rosStackDeploy(stackName, {}); expect(logger.warn).toHaveBeenCalledWith(`${lang.__('UPDATE_COMPLETELY_SAME_STACK')}`); }); it('should throw error when deploy stack failed', async () => { + const stackName = 'newStack'; + mockedGetStore.mockReturnValue({ stackName }); mockedListStacks.mockResolvedValue({ statusCode: 200, body: { stacks: [] } }); mockedCreateStack.mockResolvedValueOnce({ body: { stackId: 'newStackId' } }); mockedGetStack.mockResolvedValue({ body: { status: 'ROLLBACK_COMPLETE' } }); - await expect(rosStackDeploy('newStack', {}, context)).rejects.toThrow( + await expect(rosStackDeploy(stackName, {})).rejects.toThrow( `Stack operation failed with status: ROLLBACK_COMPLETE`, ); }); diff --git a/tests/fixtures/contextFixture.ts b/tests/fixtures/contextFixture.ts index 0a50b67..978e09f 100644 --- a/tests/fixtures/contextFixture.ts +++ b/tests/fixtures/contextFixture.ts @@ -1,7 +1,7 @@ -import { ActionContext } from '../../src/types'; +import { Context } from '../../src/types'; import { ProviderEnum } from '../../src/common'; -export const context: ActionContext = { +export const context: Context = { stage: 'test', stackName: 'testStack', provider: ProviderEnum.ALIYUN, diff --git a/tests/fixtures/deployFixture.ts b/tests/fixtures/deployFixture.ts index 09a2a2d..10e917e 100644 --- a/tests/fixtures/deployFixture.ts +++ b/tests/fixtures/deployFixture.ts @@ -784,6 +784,28 @@ export const oneFcIacWithNasRos = { 'hello_fn_nas_mount_L21udC9uYXM', ], }, + hello_fn_datasource_subnet_c3VibmV0LTEyMzQ1Ng: { + Properties: { + RefreshOptions: 'Always', + VSwitchId: 'subnet-123456', + }, + Type: 'DATASOURCE::VPC::VSwitch', + }, + hello_fn_datasource_subnet_c3VibmV0LTY3ODkw: { + Properties: { + RefreshOptions: 'Always', + VSwitchId: 'subnet-67890', + }, + Type: 'DATASOURCE::VPC::VSwitch', + }, + hello_fn_datasource_subnet_c3VibmV0LTk4NzY1: { + Properties: { + RefreshOptions: 'Always', + VSwitchId: 'subnet-98765', + }, + Type: 'DATASOURCE::VPC::VSwitch', + }, + hello_fn_nas_L21udC9uYXM: { Properties: { DeletionForce: false, @@ -825,6 +847,51 @@ export const oneFcIacWithNasRos = { }, Type: 'ALIYUN::NAS::MountTarget', }, + hello_fn_nas_rule_c3VibmV0LTEyMzQ1Ng: { + Properties: { + AccessGroupName: { + 'Fn::GetAtt': ['hello_fn_nas_access_L21udC9uYXM', 'AccessGroupName'], + }, + FileSystemType: 'standard', + Priority: 1, + RWAccessType: 'RDWR', + SourceCidrIp: { + 'Fn::GetAtt': ['hello_fn_datasource_subnet_c3VibmV0LTEyMzQ1Ng', 'CidrBlock'], + }, + UserAccessType: 'no_squash', + }, + Type: 'ALIYUN::NAS::AccessRule', + }, + hello_fn_nas_rule_c3VibmV0LTY3ODkw: { + Properties: { + AccessGroupName: { + 'Fn::GetAtt': ['hello_fn_nas_access_L21udC9uYXM', 'AccessGroupName'], + }, + FileSystemType: 'standard', + Priority: 1, + RWAccessType: 'RDWR', + SourceCidrIp: { + 'Fn::GetAtt': ['hello_fn_datasource_subnet_c3VibmV0LTY3ODkw', 'CidrBlock'], + }, + UserAccessType: 'no_squash', + }, + Type: 'ALIYUN::NAS::AccessRule', + }, + hello_fn_nas_rule_c3VibmV0LTk4NzY1: { + Properties: { + AccessGroupName: { + 'Fn::GetAtt': ['hello_fn_nas_access_L21udC9uYXM', 'AccessGroupName'], + }, + FileSystemType: 'standard', + Priority: 1, + RWAccessType: 'RDWR', + SourceCidrIp: { + 'Fn::GetAtt': ['hello_fn_datasource_subnet_c3VibmV0LTk4NzY1', 'CidrBlock'], + }, + UserAccessType: 'no_squash', + }, + Type: 'ALIYUN::NAS::AccessRule', + }, hello_fn_security_group: { Properties: { SecurityGroupEgress: [ diff --git a/tests/stack/deploy.test.ts b/tests/stack/deploy.test.ts index 8ffd4bd..d4eb139 100644 --- a/tests/stack/deploy.test.ts +++ b/tests/stack/deploy.test.ts @@ -1,5 +1,4 @@ import { deployStack } from '../../src/stack'; -import { ActionContext } from '../../src/types'; import { bucketMinimumIac, bucketMinimumRos, @@ -27,12 +26,22 @@ import { } from '../fixtures/deployFixture'; import { cloneDeep, set } from 'lodash'; +const mockedEnterWith = jest.fn(); +const mockedGetStore = jest.fn(); + const mockedRosStackDeploy = jest.fn(); const mockedResolveCode = jest.fn(); const mockedPublishAssets = jest.fn(); const mockedCleanupAssets = jest.fn(); const mockedGetIamInfo = jest.fn(); +jest.mock('node:async_hooks', () => ({ + AsyncLocalStorage: jest.fn().mockImplementation(() => ({ + enterWith: (...args: unknown[]) => mockedEnterWith(...args), + getStore: (...args: unknown[]) => mockedGetStore(...args), + })), +})); + jest.mock('../../src/common', () => ({ ...jest.requireActual('../../src/common'), rosStackDeploy: (...args: unknown[]) => mockedRosStackDeploy(...args), @@ -50,6 +59,7 @@ describe('Unit tests for stack deployment', () => { mockedGetIamInfo.mockResolvedValueOnce({ accountId: '123456789012', region: 'cn-hangzhou' }); }); afterEach(() => { + mockedEnterWith.mockRestore(); mockedRosStackDeploy.mockRestore(); mockedResolveCode.mockRestore(); mockedPublishAssets.mockRestore(); @@ -59,43 +69,44 @@ describe('Unit tests for stack deployment', () => { it('should deploy generated stack when minimum fields provided', async () => { const stackName = 'my-demo-minimum-stack'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValueOnce(stackName); - await deployStack(stackName, minimumIac, { stackName } as ActionContext); + await deployStack(stackName, minimumIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, minimumRos, { stackName }]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, minimumRos]); }); it('should deploy generated stack when only one FC specified', async () => { const stackName = 'my-demo-stack-fc-only'; + mockedGetStore.mockReturnValue({ stackName }); + mockedRosStackDeploy.mockResolvedValueOnce(stackName); - await deployStack(stackName, oneFcIac, { stackName } as ActionContext); + await deployStack(stackName, oneFcIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, oneFcRos, { stackName }]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, oneFcRos]); }); it('should reference to default stage mappings when --stage not provided', async () => { const options = { stackName: 'my-demo-stack-fc-with-stage-1', stage: 'default' }; + mockedGetStore.mockReturnValue(options); mockedRosStackDeploy.mockResolvedValueOnce(options.stackName); - await deployStack(options.stackName, oneFcIacWithStage, options as ActionContext); + await deployStack(options.stackName, oneFcIacWithStage); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - options.stackName, - oneFcWithStageRos, - options, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([options.stackName, oneFcWithStageRos]); }); it('should reference to specified stage mappings when --stage is provided', async () => { const options = { stackName: 'my-demo-stack-fc-with-stage-1', stage: 'dev' }; + mockedGetStore.mockReturnValue(options); mockedRosStackDeploy.mockResolvedValueOnce(options.stackName); - await deployStack(options.stackName, oneFcIacWithStage, options as ActionContext); + await deployStack(options.stackName, oneFcIacWithStage); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ @@ -105,26 +116,23 @@ describe('Unit tests for stack deployment', () => { 'Resources.hello_fn.Properties.EnvironmentVariables.NODE_ENV.Fn::FindInMap', ['stages', 'dev', 'node_env'], ), - options, ]); }); it('should evaluate service name as pure string when it reference ${ctx.stage}', async () => { const options = { stackName: 'my-demo-stack-fc-with-stage-1', stage: 'dev' }; + mockedGetStore.mockReturnValue(options); mockedRosStackDeploy.mockResolvedValueOnce(options.stackName); - await deployStack(options.stackName, referredServiceIac, options as ActionContext); + await deployStack(options.stackName, referredServiceIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - options.stackName, - referredServiceRos, - options, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([options.stackName, referredServiceRos]); }); it('should create bucket and store code artifact to bucket when code size > 15MB', async () => { const stackName = 'my-large-code-stack'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValueOnce(stackName); await deployStack( @@ -134,7 +142,6 @@ describe('Unit tests for stack deployment', () => { 'functions[0].code.path', 'tests/fixtures/artifacts/large-artifact.zip', ), - { stackName } as ActionContext, ); const expectedAssets = Array(2).fill({ @@ -144,125 +151,96 @@ describe('Unit tests for stack deployment', () => { }); expect(mockedPublishAssets).toHaveBeenCalledTimes(1); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedPublishAssets).toHaveBeenCalledWith(expectedAssets, { stackName }); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - largeCodeRos, - { - stackName, - }, - ]); + expect(mockedPublishAssets).toHaveBeenCalledWith(expectedAssets); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, largeCodeRos]); expect(mockedCleanupAssets).toHaveBeenCalledTimes(1); - expect(mockedCleanupAssets).toHaveBeenCalledWith(expectedAssets, { stackName }); + expect(mockedCleanupAssets).toHaveBeenCalledWith(expectedAssets); }); describe('unit test for deploy of events', () => { it('should deploy event with custom domain specified when domain is provided', async () => { const stackName = 'my-event-stack-with-custom-domain'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, oneFcOneGatewayIac, { stackName } as ActionContext); + await deployStack(stackName, oneFcOneGatewayIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - oneFcOneGatewayRos, - { stackName }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, oneFcOneGatewayRos]); }); }); describe('unit test for deploy of databases', () => { it('should deploy elasticsearch serverless when database minimum fields provided', async () => { const stackName = 'my-demo-es-serverless-stack'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, esServerlessMinimumIac, { stackName } as ActionContext); + await deployStack(stackName, esServerlessMinimumIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - esServerlessMinimumRos, - { - stackName, - }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, esServerlessMinimumRos]); }); }); describe('unit test for deploy of buckets', () => { it('should deploy bucket when minimum fields provided', async () => { const stackName = 'my-demo-bucket-stack'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, bucketMinimumIac, { stackName } as ActionContext); + await deployStack(stackName, bucketMinimumIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - bucketMinimumRos, - { stackName }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, bucketMinimumRos]); }); it('should deploy bucket as a website when website field is provided', async () => { const stackName = 'my-website-bucket-stack'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, bucketWithWebsiteIac, { stackName } as ActionContext); + await deployStack(stackName, bucketWithWebsiteIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); expect(mockedPublishAssets).toHaveBeenCalledTimes(1); expect(mockedCleanupAssets).toHaveBeenCalledTimes(1); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - bucketWithWebsiteRos, - { stackName }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, bucketWithWebsiteRos]); }); }); 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'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, oneFcIacWithNas, { stackName } as ActionContext); + await deployStack(stackName, oneFcIacWithNas); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - oneFcIacWithNasRos, - { stackName }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, oneFcIacWithNasRos]); }); it('should deploy function with container when container field is provided', async () => { const stackName = 'my-demo-stack-with-container'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, oneFcWithContainerIac, { stackName } as ActionContext); + await deployStack(stackName, oneFcWithContainerIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - oneFcWithContainerRos, - { stackName }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, oneFcWithContainerRos]); }); it('should deploy function with gpu configured', async () => { const stackName = 'my-demo-stack-with-gpu'; + mockedGetStore.mockReturnValue({ stackName }); mockedRosStackDeploy.mockResolvedValue(stackName); - await deployStack(stackName, oneFcWithGpuIac, { stackName } as ActionContext); + await deployStack(stackName, oneFcWithGpuIac); expect(mockedRosStackDeploy).toHaveBeenCalledTimes(2); - expect(mockedRosStackDeploy.mock.calls[1]).toEqual([ - stackName, - oneFcWithGpuRos, - { stackName }, - ]); + expect(mockedRosStackDeploy.mock.calls[1]).toEqual([stackName, oneFcWithGpuRos]); }); }); });