diff --git a/src/commands/index.ts b/src/commands/index.ts index 7d8aa0fa..3978f898 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -5,6 +5,7 @@ import { lang } from '../lang'; import { logger, getVersion } from '../common'; import { validate } from './validate'; import { deploy } from './deploy'; +import { template } from './template'; const program = new Command(); @@ -48,8 +49,17 @@ program {}, ) .action(async (stackName, { file, parameter, stage }) => { - logger.debug('log command info'); await deploy(stackName, { location: file, parameters: parameter, stage }); }); +program + .command('template ') + .description('print ROS template') + .option('-f, --file ', 'specify the yaml file') + .option('-s, --stage ', 'specify the stage') + .option('-t, --format ', 'output content type (JSON or YAML)', 'JSON') + .action((stackName, { format, file, stage }) => { + template(stackName, { format, location: file, stage }); + }); + program.parse(); diff --git a/src/commands/template.ts b/src/commands/template.ts new file mode 100644 index 00000000..80a5671f --- /dev/null +++ b/src/commands/template.ts @@ -0,0 +1,21 @@ +import { TemplateFormat } from '../types'; +import yaml from 'yaml'; +import { generateStackTemplate } from '../stack/deploy'; +import { constructActionContext, logger } from '../common'; +import { parseYaml } from '../stack'; + +export const template = ( + stackName: string, + options: { format: TemplateFormat; location: string; stage: string | undefined }, +) => { + const context = constructActionContext({ ...options, stackName }); + const iac = parseYaml(context.iacLocation); + const { template } = generateStackTemplate(stackName, iac, context); + + const output = + options.format === TemplateFormat.JSON + ? JSON.stringify(template, null, 2) + : yaml.stringify(template); + + logger.info(`\n${output}`); +}; diff --git a/src/common/actionContext.ts b/src/common/actionContext.ts index d3efea02..bf8d2dbb 100644 --- a/src/common/actionContext.ts +++ b/src/common/actionContext.ts @@ -23,7 +23,7 @@ export const constructActionContext = (config?: { iacLocation: (() => { const projectRoot = path.resolve(process.cwd()); return config?.location - ? path.resolve(projectRoot, config?.location) + ? path.resolve(projectRoot, config.location) : path.resolve(projectRoot, 'serverlessinsight.yml') || path.resolve(projectRoot, 'serverlessInsight.yml') || path.resolve(projectRoot, 'ServerlessInsight.yml') || diff --git a/src/stack/deploy.ts b/src/stack/deploy.ts index 62eb09c3..a053cc6b 100644 --- a/src/stack/deploy.ts +++ b/src/stack/deploy.ts @@ -3,7 +3,11 @@ import { ActionContext, ServerlessIac } from '../types'; import { logger, rosStackDeploy } from '../common'; import { IacStack } from './iacStack'; -const generateStackTemplate = (stackName: string, iac: ServerlessIac, context: ActionContext) => { +export const generateStackTemplate = ( + stackName: string, + iac: ServerlessIac, + context: ActionContext, +) => { const app = new ros.App(); new IacStack(app, iac, context); diff --git a/src/types.ts b/src/types.ts index 32aa0c6c..5ee483c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -79,3 +79,8 @@ export type ActionContext = { parameters?: Array<{ key: string; value: string }>; tags?: Array<{ key: string; value: string }>; }; + +export enum TemplateFormat { + YAML = 'YAML', + JSON = 'JSON', +} diff --git a/tests/commands/template.test.ts b/tests/commands/template.test.ts new file mode 100644 index 00000000..5005ff33 --- /dev/null +++ b/tests/commands/template.test.ts @@ -0,0 +1,35 @@ +import { template } from '../../src/commands/template'; +import { TemplateFormat } from '../../src/types'; +import { jsonTemplate, yamlTemplate } from '../fixtures/templateFixture'; + +const mockedLogger = jest.fn(); +jest.mock('../../src/common/logger', () => ({ + logger: { info: (...args: unknown[]) => mockedLogger(...args), debug: jest.fn() }, +})); +const stackName = 'printTemplateStack'; +const location = 'tests/fixtures/serverless-insight.yml'; + +describe('Unit test for template command', () => { + beforeEach(() => { + mockedLogger.mockRestore(); + }); + it('should print the template in JSON format by default', () => { + const options = { + format: TemplateFormat.JSON, + location, + stage: undefined, + }; + + template(stackName, options); + + expect(mockedLogger).toHaveBeenCalledWith(`\n${JSON.stringify(jsonTemplate, null, 2)}`); + }); + + it('should print the template in YAML format when specified', () => { + const options = { format: TemplateFormat.YAML, location, stage: undefined }; + + template(stackName, options); + + expect(mockedLogger).toHaveBeenCalledWith(yamlTemplate); + }); +}); diff --git a/tests/fixtures/serverless-insight.yml b/tests/fixtures/serverless-insight.yml index a9d7dba7..abad28e9 100644 --- a/tests/fixtures/serverless-insight.yml +++ b/tests/fixtures/serverless-insight.yml @@ -26,7 +26,7 @@ functions: name: insight-poc-fn runtime: nodejs18 handler: ${vars.handler} - code: artifacts/artifact.zip + code: tests/fixtures/artifacts/artifact.zip memory: 512 timeout: 10 environment: diff --git a/tests/fixtures/templateFixture.ts b/tests/fixtures/templateFixture.ts new file mode 100644 index 00000000..c2cdeb41 --- /dev/null +++ b/tests/fixtures/templateFixture.ts @@ -0,0 +1,281 @@ +export const jsonTemplate = { + Description: 'insight-poc stack', + Metadata: { + 'ALIYUN::ROS::Interface': { + TemplateTags: ['Create by ROS CDK'], + }, + }, + ROSTemplateFormatVersion: '2015-09-01', + Parameters: { + region: { + Type: 'String', + Default: 'cn-hangzhou', + }, + testv: { + Type: 'String', + Default: 'testVarValue', + }, + handler: { + Type: 'String', + Default: 'index.handler', + }, + }, + Mappings: { + stages: { + default: { + region: { + Ref: 'region', + }, + node_env: 'default', + }, + dev: { + region: { + Ref: 'region', + }, + node_env: 'development', + }, + prod: { + region: 'cn-shanghai', + }, + }, + }, + Resources: { + insight_poc_fn: { + Type: 'ALIYUN::FC3::Function', + Properties: { + FunctionName: 'insight-poc-fn', + Handler: { + Ref: 'handler', + }, + Runtime: 'nodejs18', + Code: { + ZipFile: + 'UEsDBBQAAAAAADiNNVkAAAAAAAAAAAAAAAAJACAAYXJ0aWZhY3QvVVQNAAfdlO5m7pTuZt2U7mZ1eAsAAQT1AQAABBQAAABQSwMEFAAIAAgA44w1WQAAAAAAAAAAngAAABEAIABhcnRpZmFjdC9pbmRleC5qc1VUDQAHO5TuZt6U7mbdlO5mdXgLAAEE9QEAAAQUAAAAhYuxCsJAEAX7fMV2uUDIDwR7rS2sz8uaBB+7srsJAfHf5cDebhhm+HiphQ9LlglsdKLHJiVWFUq8s0RPRSX4qJCBey7Pjt4NVe0KHqBzaq9sOxvY/SK+zkvQmQGlmxqmthtr/7uTbEBP/5fP2HwBUEsHCG5M74NvAAAAngAAAFBLAQIUAxQAAAAAADiNNVkAAAAAAAAAAAAAAAAJACAAAAAAAAAAAADtQQAAAABhcnRpZmFjdC9VVA0AB92U7mbulO5m3ZTuZnV4CwABBPUBAAAEFAAAAFBLAQIUAxQACAAIAOOMNVluTO+DbwAAAJ4AAAARACAAAAAAAAAAAACkgUcAAABhcnRpZmFjdC9pbmRleC5qc1VUDQAHO5TuZt6U7mbdlO5mdXgLAAEE9QEAAAQUAAAAUEsFBgAAAAACAAIAtgAAABUBAAAAAA==', + }, + EnvironmentVariables: { + NODE_ENV: { + 'Fn::FindInMap': ['stages', 'default', 'node_env'], + }, + TEST_VAR: { + Ref: 'testv', + }, + TEST_VAR_EXTRA: { + 'Fn::Sub': 'abcds-${testv}-andyou', + }, + }, + MemorySize: 512, + Timeout: 10, + }, + }, + 'insight-poc_role': { + Type: 'ALIYUN::RAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '1', + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: ['apigateway.aliyuncs.com'], + }, + }, + ], + }, + RoleName: 'insight-poc-gateway-access-role', + Description: 'insight-poc role', + Policies: [ + { + PolicyName: 'insight-poc-policy', + PolicyDocument: { + Version: '1', + Statement: [ + { + Action: ['fc:InvokeFunction'], + Resource: ['*'], + Effect: 'Allow', + }, + ], + }, + }, + ], + }, + }, + 'insight-poc_apigroup': { + Type: 'ALIYUN::ApiGateway::Group', + Properties: { + GroupName: 'insight-poc_apigroup', + Tags: [ + { + Value: 'ServerlessInsight', + Key: 'iac-provider', + }, + { + Value: 'geek-fun', + Key: 'owner', + }, + ], + }, + }, + gateway_event_api_get__api_hello: { + Type: 'ALIYUN::ApiGateway::Api', + Properties: { + ApiName: 'insight-poc-gateway_api_get__api_hello', + GroupId: { + 'Fn::GetAtt': ['insight-poc_apigroup', 'GroupId'], + }, + RequestConfig: { + RequestPath: '/api/hello', + RequestMode: 'PASSTHROUGH', + RequestProtocol: 'HTTP', + RequestHttpMethod: 'GET', + }, + ServiceConfig: { + FunctionComputeConfig: { + FcVersion: '3.0', + FcRegionId: 'cn-hangzhou', + RoleArn: { + 'Fn::GetAtt': ['insight-poc_role', 'Arn'], + }, + FunctionName: { + 'Fn::GetAtt': ['insight_poc_fn', 'FunctionName'], + }, + }, + ServiceProtocol: 'FunctionCompute', + }, + Visibility: 'PRIVATE', + ResultSample: 'ServerlessInsight resultSample', + ResultType: 'JSON', + Tags: [ + { + Value: 'ServerlessInsight', + Key: 'iac-provider', + }, + { + Value: 'geek-fun', + Key: 'owner', + }, + ], + }, + }, + }, +}; + +export const yamlTemplate = ` +Description: insight-poc stack +Metadata: + ALIYUN::ROS::Interface: + TemplateTags: + - Create by ROS CDK +ROSTemplateFormatVersion: 2015-09-01 +Parameters: + region: + Type: String + Default: cn-hangzhou + testv: + Type: String + Default: testVarValue + handler: + Type: String + Default: index.handler +Mappings: + stages: + default: + region: + Ref: region + node_env: default + dev: + region: + Ref: region + node_env: development + prod: + region: cn-shanghai +Resources: + insight_poc_fn: + Type: ALIYUN::FC3::Function + Properties: + FunctionName: insight-poc-fn + Handler: + Ref: handler + Runtime: nodejs18 + Code: + ZipFile: UEsDBBQAAAAAADiNNVkAAAAAAAAAAAAAAAAJACAAYXJ0aWZhY3QvVVQNAAfdlO5m7pTuZt2U7mZ1eAsAAQT1AQAABBQAAABQSwMEFAAIAAgA44w1WQAAAAAAAAAAngAAABEAIABhcnRpZmFjdC9pbmRleC5qc1VUDQAHO5TuZt6U7mbdlO5mdXgLAAEE9QEAAAQUAAAAhYuxCsJAEAX7fMV2uUDIDwR7rS2sz8uaBB+7srsJAfHf5cDebhhm+HiphQ9LlglsdKLHJiVWFUq8s0RPRSX4qJCBey7Pjt4NVe0KHqBzaq9sOxvY/SK+zkvQmQGlmxqmthtr/7uTbEBP/5fP2HwBUEsHCG5M74NvAAAAngAAAFBLAQIUAxQAAAAAADiNNVkAAAAAAAAAAAAAAAAJACAAAAAAAAAAAADtQQAAAABhcnRpZmFjdC9VVA0AB92U7mbulO5m3ZTuZnV4CwABBPUBAAAEFAAAAFBLAQIUAxQACAAIAOOMNVluTO+DbwAAAJ4AAAARACAAAAAAAAAAAACkgUcAAABhcnRpZmFjdC9pbmRleC5qc1VUDQAHO5TuZt6U7mbdlO5mdXgLAAEE9QEAAAQUAAAAUEsFBgAAAAACAAIAtgAAABUBAAAAAA== + EnvironmentVariables: + NODE_ENV: + Fn::FindInMap: + - stages + - default + - node_env + TEST_VAR: + Ref: testv + TEST_VAR_EXTRA: + Fn::Sub: abcds-\${testv}-andyou + MemorySize: 512 + Timeout: 10 + insight-poc_role: + Type: ALIYUN::RAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "1" + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: + - apigateway.aliyuncs.com + RoleName: insight-poc-gateway-access-role + Description: insight-poc role + Policies: + - PolicyName: insight-poc-policy + PolicyDocument: + Version: "1" + Statement: + - Action: + - fc:InvokeFunction + Resource: + - "*" + Effect: Allow + insight-poc_apigroup: + Type: ALIYUN::ApiGateway::Group + Properties: + GroupName: insight-poc_apigroup + Tags: + - Value: ServerlessInsight + Key: iac-provider + - Value: geek-fun + Key: owner + gateway_event_api_get__api_hello: + Type: ALIYUN::ApiGateway::Api + Properties: + ApiName: insight-poc-gateway_api_get__api_hello + GroupId: + Fn::GetAtt: + - insight-poc_apigroup + - GroupId + RequestConfig: + RequestPath: /api/hello + RequestMode: PASSTHROUGH + RequestProtocol: HTTP + RequestHttpMethod: GET + ServiceConfig: + FunctionComputeConfig: + FcVersion: "3.0" + FcRegionId: cn-hangzhou + RoleArn: + Fn::GetAtt: + - insight-poc_role + - Arn + FunctionName: + Fn::GetAtt: + - insight_poc_fn + - FunctionName + ServiceProtocol: FunctionCompute + Visibility: PRIVATE + ResultSample: ServerlessInsight resultSample + ResultType: JSON + Tags: + - Value: ServerlessInsight + Key: iac-provider + - Value: geek-fun + Key: owner +`;