packages/ros-cdk-cli/lib/cdk-toolkit.ts (1,711 lines of code) (raw):

import * as colors from 'colors/safe'; import * as fs from 'fs'; import * as path from 'path'; import * as readlineSync from 'readline-sync'; import * as util from 'util'; import * as jszip from 'jszip'; import {format, promisify} from 'util'; import {cipher, decipher} from './util/cipher'; import {RewritableBlock} from './util/display'; import {isNumber, isString} from 'lodash'; import {exec as _exec} from 'child_process'; import Credentials, {Config} from '@alicloud/credentials'; import {CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollection} from './api/cloud-assembly'; import {CloudExecutable} from './api/cloud-executable'; import {data, error, print, success, warning} from './logging'; import {Configuration, PROJECT_CONFIG} from './settings'; import {exit} from 'process'; import {printStackDiff} from './diff'; import {deserializeStructure} from './serialize'; import {NetworkError} from './exception'; const rosClient = require('@alicloud/ros-2019-09-10'); const os = require('os'); const http = require('http'); const https = require('https'); const ossClient = require('ali-oss'); const generateSafeId = require('generate-safe-id'); const CONFIG_NAME = 'account.config.json'; const LOCAL_PATH = './'; const GLOBAL_PATH = __dirname + '/../'; const STACK_INFO = 'stack.info.json'; const DEPLOY_STACK_ID_LENGTH = 36; const CLI_CONFIG_FILE = '/.aliyun/config.json'; const INIT_STACK = 'init'; const SYNTH_STACK = 'synth'; const DEPLOY_STACK = 'deploy'; const DESTROY_STACK = 'destroy'; const PACKAGE_JSON = __dirname + '/../package.json'; const OUTPUTS_JSON = 'stack.outputs.json'; const exec = promisify(_exec); const requestOptions: { [name: string]: any } = { headers: { 'User-Agent': "ROS-CLI-" + JSON.parse(fs.readFileSync(PACKAGE_JSON).toString())['version'] + "::" + readLanguageInfo() }, timeout: 90000 }; const sleep = function (ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); }; const stream = process.stdout; let withDefaultPrinterObj: any; export interface CdkToolkitProps { /** * The Cloud Executable */ cloudExecutable: CloudExecutable; /** * Whether to be verbose * * @default false */ verbose?: boolean; /** * Don't stop on error metadata * * @default false */ ignoreErrors?: boolean; /** * Treat warnings in metadata as errors * * @default false */ strict?: boolean; /** * Application configuration (settings and context) */ configuration: Configuration; } /** * Toolkit logic * * The toolkit runs the `cloudExecutable` to obtain a cloud assembly * */ export class CdkToolkit { private static async getJson(fileName: string, key: string, allowedEmpty: boolean = false) { let filePath = LOCAL_PATH + fileName; if (fs.existsSync(filePath)) { let file = fs.readFileSync(filePath).toString(); return JSON.parse(file)[key]; } else if (fileName === STACK_INFO) { return undefined; } filePath = GLOBAL_PATH + fileName; if (fs.existsSync(filePath)) { let file = fs.readFileSync(filePath).toString(); return JSON.parse(file)[key]; } if (!allowedEmpty) { error("Please use 'ros-cdk config (-g)' to set your account configuration firstly!"); exit(1); } return null } constructor(private readonly props: CdkToolkitProps) { } public static async getConfig() { let modeType = await CdkToolkit.getJson(CONFIG_NAME, 'type', true) let endpoint = await CdkToolkit.getJson(CONFIG_NAME, 'endpoint', true) let regionId = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true) let configInfo: any; switch (modeType) { case 'ecs_ram_role': configInfo = new Config({ type: 'ecs_ram_role', roleName: await CdkToolkit.getJson(CONFIG_NAME, 'roleName') }); break; case 'sts': configInfo = new Config({ type: 'sts', accessKeyId: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'accessKeyId')), accessKeySecret: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'accessKeySecret')), securityToken: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'securityToken')) }); break; case 'ram_role_arn': configInfo = new Config({ type: 'ram_role_arn', accessKeyId: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'accessKeyId')), accessKeySecret: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'accessKeySecret')), roleArn: await CdkToolkit.getJson(CONFIG_NAME, 'roleArn'), roleSessionName: await CdkToolkit.getJson(CONFIG_NAME, 'roleSessionName') }); break; case 'access_key': configInfo = new Config({ type: 'access_key', accessKeyId: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'accessKeyId')), accessKeySecret: await decipher(await CdkToolkit.getJson(CONFIG_NAME, 'accessKeySecret')) }); break; } endpoint = endpoint ? endpoint : 'https://ros.aliyuncs.com'; let newAccessKeyId: string; let newAccessKeySecret: string; let newSecurityToken: string; // @ts-ignore newAccessKeyId = newAccessKeyId ? newAccessKeyId : process.env.ACCESS_KEY_ID // @ts-ignore newAccessKeySecret = newAccessKeySecret ? newAccessKeySecret : process.env.ACCESS_KEY_SECRET // @ts-ignore newSecurityToken = newSecurityToken ? newSecurityToken : process.env.SECURITY_TOKEN if (configInfo) { try { const cred = new Credentials(configInfo); newAccessKeyId = await cred.getAccessKeyId(); newAccessKeySecret = await cred.getAccessKeySecret(); newSecurityToken = await cred.getSecurityToken(); } catch (e) { error( 'WANRNING: Please check the accuracy of the credential information you import from CLI config!' + e.message, ); exit(1); } } if (!newAccessKeyId || !newAccessKeySecret) { error("Please use 'ros-cdk config (-g)' or set environment to set your account configuration firstly!"); exit(1); } return { endpoint: endpoint, accessKeyId: newAccessKeyId, accessKeySecret: newAccessKeySecret, securityToken: newSecurityToken, regionId: regionId } } public async getRosClient() { const config = await CdkToolkit.getConfig(); let client; if (!config.accessKeyId || !config.accessKeySecret) { error("Please use 'ros-cdk config (-g)' or set environment to set your account configuration firstly!"); exit(1); } else if (!config.securityToken) { client = new rosClient({ endpoint: config.endpoint, accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret }); } else { client = new rosClient({ endpoint: config.endpoint, accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret, securityToken: config.securityToken }); } let httpModule = client.endpoint.startsWith('https://') ? https : http; client.keepAliveAgent = new httpModule.Agent({ keepAlive: true, keepAliveMsecs: 15000 }); return client; } public async config(global: boolean) { let configSavePath = (global ? GLOBAL_PATH : LOCAL_PATH) + CONFIG_NAME; let sourceModeType: string = ''; let sourceEndpoint: string = ''; let sourceRegionId: string = ''; let sourceAccessKeyId: string = ''; let sourceAccessKeySecret: string = ''; let sourceSecurityToken: string = ''; let sourceRoleArn: string = ''; let sourceRoleSessionName: string = ''; if (fs.existsSync(configSavePath)) { let configFile = JSON.parse(fs.readFileSync(configSavePath).toString()); sourceModeType = configFile['type'] sourceEndpoint = configFile['endpoint'] sourceRegionId = configFile['regionId'] sourceAccessKeyId = configFile['accessKeyId'] sourceAccessKeySecret = configFile['accessKeySecret'] sourceSecurityToken = configFile['securityToken'] sourceRoleArn = configFile['roleArn'] sourceRoleSessionName = configFile['roleSessionName'] } let modeTypeObj = { ecs_ram_role: 'EcsRamRole', sts: 'StsToken', ram_role_arn: 'RamRoleArn', access_key: 'AK' } // @ts-ignore let defaultModeType = sourceModeType ? modeTypeObj[sourceModeType] : '' let defaultEndpoint = sourceEndpoint ? sourceEndpoint : 'https://ros.aliyuncs.com' let defaultRegionId = sourceRegionId ? sourceRegionId : 'cn-hangzhou' let modeType = ['AK', 'StsToken', 'RamRoleArn', 'EcsRamRole'] let endpoint = readlineSync.question(`Endpoint(optional, [${defaultEndpoint.toString()}]):`, {defaultInput: defaultEndpoint}); let regionId = readlineSync.question(`RegionId(optional, [${defaultRegionId.toString()}]):`, {defaultInput: defaultRegionId}); let modeIndex = readlineSync.keyInSelect(modeType, `Authenticate mode [${defaultModeType.toString()}]:`); let inputConfigInfo: any = {}; let checkCommand: string; let curlCommand: string; if (modeType[modeIndex] === 'EcsRamRole') { if (process.platform === 'win32') { checkCommand = 'powershell -Command "(curl -URi http://100.100.100.200/latest/meta-data/Ram/security-credentials/).StatusCode"'; curlCommand = 'powershell -Command "(curl -URi http://100.100.100.200/latest/meta-data/Ram/security-credentials/).Content"'; } else { checkCommand = 'curl http://100.100.100.200/latest/meta-data/Ram/security-credentials/ -o /dev/null -s -w %{http_code}'; curlCommand = 'curl http://100.100.100.200/latest/meta-data/Ram/security-credentials/'; } try { const {stdout: checkStdout} = await exec(checkCommand); if (checkStdout.trim() !== '200') { error( 'WANRNING: If want to Use EcsRamRole config, Please bind EcsRamRole to the host.', ); exit(1); } } catch (e) { error( 'WANRNING: If want to Use EcsRamRole config, Please bind EcsRamRole to the host!' + e.message, ); exit(1); } const {stdout: curlStdout} = await exec(curlCommand); let roleName = readlineSync.question(`RoleName, The configured role of the host: [${curlStdout.trim()}]`, {defaultInput: curlStdout.trim()}); inputConfigInfo = { type: 'ecs_ram_role', roleName: roleName }; } else if (modeType[modeIndex] === 'StsToken') { let accessKeyId: string; let accessKeySecret: string; let securityToken: string; if (sourceModeType === 'sts') { let defaultAccessKeyId = desensitization(await decipher(sourceAccessKeyId.toString())) let defaultAccessKeySecret = desensitization(await decipher(sourceAccessKeySecret.toString())) let defaultSecurityToken = desensitization(await decipher(sourceSecurityToken.toString())) accessKeyId = readlineSync.question(`AccessKeyId [${defaultAccessKeyId}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceAccessKeyId.toString()) }); accessKeySecret = readlineSync.question(`AccessKeySecret [${defaultAccessKeySecret}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceAccessKeySecret.toString()) }); securityToken = readlineSync.question(`SecurityToken [${defaultSecurityToken}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceSecurityToken.toString()) }); } else { accessKeyId = readlineSync.question('AccessKeyId:', {hideEchoBack: true}); accessKeySecret = readlineSync.question('AccessKeySecret:', {hideEchoBack: true}); securityToken = readlineSync.question('SecurityToken:', {hideEchoBack: true}); } inputConfigInfo = { type: 'sts', accessKeyId: await cipher(accessKeyId), accessKeySecret: await cipher(accessKeySecret), securityToken: await cipher(securityToken) }; } else if (modeType[modeIndex] === 'RamRoleArn') { let accessKeyId: string; let accessKeySecret: string; let roleArn: string; let roleSessionName: string; if (sourceModeType === 'ram_role_arn') { let defaultAccessKeyId = desensitization(await decipher(sourceAccessKeyId.toString())) let defaultAccessKeySecret = desensitization(await decipher(sourceAccessKeySecret.toString())) let defaultRoleArn = sourceRoleArn.toString() let defaultRoleSessionName = sourceRoleSessionName.toString() accessKeyId = readlineSync.question(`AccessKeyId [${defaultAccessKeyId}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceAccessKeyId.toString()) }); accessKeySecret = readlineSync.question(`AccessKeySecret [${defaultAccessKeySecret}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceAccessKeySecret.toString()) }); roleArn = readlineSync.question(`RoleArn [${defaultRoleArn}]:`, { defaultInput: defaultRoleArn }); roleSessionName = readlineSync.question(`RoleSessionName [${defaultRoleSessionName}]:`, { defaultInput: defaultRoleSessionName }); } else { accessKeyId = readlineSync.question('AccessKeyId:', {hideEchoBack: true}); accessKeySecret = readlineSync.question('AccessKeySecret:', {hideEchoBack: true}); roleArn = readlineSync.question('RoleArn(eg: acs:ram::******:role/******):'); roleSessionName = readlineSync.question('RoleSessionName:'); } inputConfigInfo = { type: 'ram_role_arn', accessKeyId: await cipher(accessKeyId), accessKeySecret: await cipher(accessKeySecret), roleArn: roleArn, roleSessionName: roleSessionName }; } else if (modeType[modeIndex] === 'AK') { let accessKeyId: string; let accessKeySecret: string; if (sourceModeType === 'access_key') { let defaultAccessKeyId = desensitization(await decipher(sourceAccessKeyId.toString())) let defaultAccessKeySecret = desensitization(await decipher(sourceAccessKeySecret.toString())) accessKeyId = readlineSync.question(`AccessKeyId [${defaultAccessKeyId}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceAccessKeyId.toString()) }); accessKeySecret = readlineSync.question(`AccessKeySecret [${defaultAccessKeySecret}]:`, { hideEchoBack: true, defaultInput: await decipher(sourceAccessKeySecret.toString()) }); } else { accessKeyId = readlineSync.question('AccessKeyId:', {hideEchoBack: true}); accessKeySecret = readlineSync.question('AccessKeySecret:', {hideEchoBack: true}); } inputConfigInfo = { type: 'access_key', accessKeyId: await cipher(accessKeyId), accessKeySecret: await cipher(accessKeySecret) }; } else { error( 'WANRNING: If want to deploy or delete stack, a certification method must be selected', ); exit(1); } inputConfigInfo.endpoint = endpoint inputConfigInfo.regionId = regionId let file = path.join(configSavePath); fs.writeFileSync(file, JSON.stringify(inputConfigInfo, null, '\t')); success(`\n ✅ Your cdk configuration has been saved successfully!`); exit(0); } public async configSet(options: ConfigSetOptions) { let configSavePath = (options.global ? GLOBAL_PATH : LOCAL_PATH) + CONFIG_NAME; let modeType = options.mode; let configInfo: any = {}; let ak = options.ak ? options.ak : ''; let sk = options.sk ? options.sk : ''; let stsToken = options.sts ? options.sts : ''; let ramRoleArn = options.ramRoleArn ? options.ramRoleArn : ''; let roleSessionName = options.roleSessionName ? options.roleSessionName : ''; let ramRoleName = options.ramRoleName ? options.ramRoleName : ''; if (modeType === 'AK') { configInfo = { type: 'access_key', accessKeyId: await cipher(ak), accessKeySecret: await cipher(sk) }; } else if (modeType === 'StsToken') { configInfo = { type: 'sts', accessKeyId: await cipher(ak), accessKeySecret: await cipher(sk), securityToken: await cipher(stsToken) }; } else if (modeType === 'RamRoleArn') { configInfo = { type: 'ram_role_arn', accessKeyId: await cipher(ak), accessKeySecret: await cipher(sk), roleArn: ramRoleArn, roleSessionName: roleSessionName }; } else if (modeType === 'EcsRamRole') { configInfo = { type: 'ecs_ram_role', roleName: ramRoleName }; } else { error( 'WANRNING: If want to deploy or delete stack, a authenticate mode must be in (AK|StsToken|RamRoleArn|EcsRamRole)', ); exit(1); } let file = path.join(configSavePath); configInfo.regionId = options.region; configInfo.endpoint = options.endpoint; fs.writeFileSync(file, JSON.stringify(configInfo, null, '\t')); success(`\n ✅ Your cdk configuration has been saved successfully!`); exit(0); } public async loadCliConfig(options: LoadConfigOptions) { let configSavePath = (options.global ? GLOBAL_PATH : LOCAL_PATH) + CONFIG_NAME; let configureInfos = await this.getCliConfig(options.loadFilePath) let modeType = Object.keys(configureInfos); let modeIndex = readlineSync.keyInSelect(modeType, 'Select authenticate mode:'); let profileNames: string[] = []; let profileConfig: any = {}; if (modeIndex === -1) { error( 'WANRNING: If want to deploy or delete stack, a certification method must be selected', ); exit(1); } else if (modeType[modeIndex] === 'AK') { profileNames = configureInfos.AK.map((item: { name: any; }) => item.name) } else if (modeType[modeIndex] === 'StsToken') { profileNames = configureInfos.StsToken.map((item: { name: any; }) => item.name) } else if (modeType[modeIndex] === 'RamRoleArn') { profileNames = configureInfos.RamRoleArn.map((item: { name: any; }) => item.name) } else if (modeType[modeIndex] === 'EcsRamRole') { profileNames = configureInfos.EcsRamRole.map((item: { name: any; }) => item.name) } let profileIndex = readlineSync.keyInSelect(profileNames, 'Select Authenticate profile name:'); if (profileIndex === -1) { error( 'WANRNING: If want to deploy or delete stack, a certification profile must be selected', ); exit(1); } let endpoint = await CdkToolkit.getJson(CONFIG_NAME, 'endpoint', true) let regionId; let configInfo: any = {}; endpoint = endpoint ? endpoint : 'https://ros.aliyuncs.com'; switch (modeType[modeIndex]) { case 'AK': profileConfig = configureInfos.AK.find((profiles: { name: string; }) => profiles.name === profileNames[profileIndex]); if (!profileConfig['accessKeyId'] || !profileConfig['accessKeySecret']) { error( 'WANRNING: If want to deploy or delete stack, accessKeyId or accessKeySecret Cannot be empty!', ); exit(1); } configInfo = { type: 'access_key', accessKeyId: await cipher(profileConfig['accessKeyId']), accessKeySecret: await cipher(profileConfig['accessKeySecret']) }; break; case 'StsToken': profileConfig = configureInfos.StsToken.find((profiles: { name: string; }) => profiles.name === profileNames[profileIndex]); if (!profileConfig['accessKeyId'] || !profileConfig['accessKeySecret'] || !profileConfig['securityToken']) { error( 'WANRNING: If want to deploy or delete stack, accessKeyId, accessKeySecret or securityToken Cannot be empty!', ); exit(1); } configInfo = { type: 'sts', accessKeyId: await cipher(profileConfig['accessKeyId']), accessKeySecret: await cipher(profileConfig['accessKeySecret']), securityToken: await cipher(profileConfig['securityToken']) }; break; case 'RamRoleArn': profileConfig = configureInfos.RamRoleArn.find((profiles: { name: string; }) => profiles.name === profileNames[profileIndex]); if (!profileConfig['accessKeyId'] || !profileConfig['accessKeySecret'] || !profileConfig['roleArn'] || !profileConfig['roleSessionName']) { error( 'WANRNING: If want to deploy or delete stack, accessKeyId, accessKeySecret, roleArn or roleSessionName Cannot be empty!', ); exit(1); } configInfo = { type: 'ram_role_arn', accessKeyId: await cipher(profileConfig['accessKeyId']), accessKeySecret: await cipher(profileConfig['accessKeySecret']), roleArn: profileConfig['roleArn'], roleSessionName: profileConfig['roleSessionName'] }; break; case 'EcsRamRole': profileConfig = configureInfos.EcsRamRole.find((profiles: { name: string; }) => profiles.name === profileNames[profileIndex]); if (!profileConfig['roleName']) { error( 'WANRNING: If want to deploy or delete stack, roleName Cannot be empty!', ); exit(1); } configInfo = { type: 'ecs_ram_role', roleName: profileConfig['roleName'] }; break; } regionId = profileConfig['region'] ? profileConfig['region'] : 'cn-hangzhou'; configInfo.regionId = regionId configInfo.endpoint = endpoint let file = path.join(configSavePath); fs.writeFileSync(file, JSON.stringify(configInfo, null, '\t')); success(`\n ✅ Your cdk configuration has been load from Aliyun Cli configuration saved successfully %s %s!`, modeType[modeIndex], profileNames[profileIndex]); exit(0); } public async list(selectors: string[]) { await this.syncStackInfo(); const stacks = await this.selectAllStacksForDefault(selectors); for (const stack of stacks.stackArtifacts) { let stackInfo = await this.findStackInfo(stack.id); let status = stackInfo.status data(stack.id, status.toString()); } } /** * Synthesize the given set of stacks (called when the user runs 'cdk synth') * * INPUT: Stack names can be supplied using a glob filter. If no stacks are * given, all stacks from the application are implictly selected. * * OUTPUT: If more than one stack ends up being selected, an output directory * should be supplied, where the templates will be written. */ public async synth(stackNames: string[], exclusively: boolean): Promise<any> { await this.syncStackInfo(); const stacks = await this.selectStacksForDiff(stackNames, exclusively); // if we have a single stack, print it to STDOUT if (stacks.stackCount === 1) { await this.updateStackInfo(stacks.firstStack.id, SYNTH_STACK, undefined); return stacks.firstStack.template; } else { for (let stack of stacks.stackArtifacts) { await this.updateStackInfo(stack.id, SYNTH_STACK, undefined); } } const isIntegMode = process.env.CDK_INTEG_MODE === '1'; if (isIntegMode) { return stacks.stackArtifacts.map((s) => s.template); } // not outputting template to stdout, let's explain things to the user a little bit... success(`Successfully synthesized to ${colors.blue(path.resolve(stacks.assembly.directory))}`); print( `Supply a stack id (${stacks.stackArtifacts.map((s) => colors.green(s.id)).join(', ')}) to display its template.`, ); return undefined; } public async publishAssets(assetsJson: { [key: string]: any }) { const files = assetsJson['files']; const filesNum = Object.keys(files).length; if (filesNum === 0) { return; } if (filesNum === 1) { const fileKey = Object.keys(files)[0]; const file = files[fileKey]; if (file.source && file.source.path.endsWith('.template.json')) { return; } } const config = await CdkToolkit.getConfig(); let client_params; if (!config.accessKeyId || !config.accessKeySecret) { error("Please use 'ros-cdk config (-g)' or set environment to set your account configuration firstly!"); exit(1); } else if (!config.securityToken) { client_params = { region: `oss-${config.regionId}`, accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret }; } else { client_params = { region: `oss-${config.regionId}`, accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret, securityToken: config.securityToken }; } const client = new ossClient(client_params); let bucketName; let bucketExists = false; const options = { storageClass: 'Standard', acl: 'private', dataRedundancyType: 'LRS' } const readFiles = (folder: string, zip: jszip) => { const zipFiles = fs.readdirSync(folder); zipFiles.forEach((file) => { const filePath = path.join(folder, file); if (fs.statSync(filePath).isDirectory()) { const subZip = zip.folder(file); if (subZip) { readFiles(filePath, subZip); } } else { const content = fs.readFileSync(filePath); zip.file(file, content); } }); }; for (const key of Object.keys(files)) { const source = files[key]['source']; const destination = files[key]['destinations']; let assetPath = `./cdk.out/${source['path']}`; const objectKey = destination['current_account-current_region']['objectKey']; if (source['packaging'] === 'zip') { const zip = new jszip(); readFiles(assetPath, zip); assetPath = `./cdk.out/${source['path']}.zip`; await zip.generateAsync({ type: 'nodebuffer' }).then((content) => { fs.writeFileSync(assetPath, content); console.log('Folder compressed successfully!'); }).catch((e) => { error('Error compressing folder:\n', e); }); } if (!bucketExists) { bucketName = destination['current_account-current_region']['bucketName'].replace('${ALIYUN::Region}', config.regionId); try { await client.putBucket(bucketName, options); bucketExists = true; console.log(`Create bucket(${bucketName}) successfully!`); } catch (e) { error(`Error create bucket(${bucketName}):\n`, e); exit(1); } } const store = new ossClient({bucket: bucketName, ...client_params}); const headers = { 'x-oss-storage-class': 'Standard', 'x-oss-object-acl': 'private', 'x-oss-forbid-overwrite': 'false' }; try { await store.put(objectKey, path.normalize(assetPath), headers) console.log(`Upload file(${assetPath}) to bucket(${bucketName}) successfully!`); } catch (e) { error(`Error upload file(${assetPath}) to bucket(${bucketName}):\n`, e); exit(1); } } return bucketName; } public async deploy(options: DeployOptions) { let templateBody: any; let sync = options.sync let outputs = options.outputsFile let skipIfNoChanges = options.skipIfNoChanges let resourceGroupId = options.resourceGroupId let detailLog = options.detailLog let disableRollback = options.disableRollback let config_region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); config_region = config_region ? config_region : process.env.REGION_ID; await this.syncStackInfo(); const stacks = await this.selectStacksForDeploy(options.stackNames, options.exclusively); const client = await this.getRosClient(); let exitSignal = 0; for (let stack of stacks.stackArtifacts) { let stackName = stack.stackName; let regionId = options.regionId; if (regionId === 'default') { regionId = config_region; } let bucketName; if (fs.existsSync(`./cdk.out/${stackName}.assets.json`)) { let assetsFileBody = fs.readFileSync(`./cdk.out/${stackName}.assets.json`); let assetsJson = JSON.parse(assetsFileBody.toString('utf-8').trim()); bucketName = await this.publishAssets(assetsJson) } let templateFileBody = fs.readFileSync(`./cdk.out/${stackName}.template.json`); let ClientToken = generateSafeId(); let templateBodyBase64Data = templateFileBody.toString('base64').trim(); let tmpSignal = 0; if (Buffer.byteLength(templateFileBody, 'utf8') < 524273) { templateBody = `@Base64Encoded: ${templateBodyBase64Data}` } else { templateBody = templateFileBody } let content: { [name: string]: any } = { StackName: stackName.toString(), TimeoutInMinutes: options.timeout, TemplateBody: templateBody, ClientToken: ClientToken, DisableRollback: disableRollback }; requestOptions['method'] = 'POST' if (resourceGroupId) { content['ResourceGroupId'] = resourceGroupId } if (stack.tags) { let count: number = 1; let paras = stack.tags; for (let key in paras) { content['Tags.' + count.toString() + '.Key'] = key; content['Tags.' + count.toString() + '.Value'] = paras[key]; count++; } } if (options.parameters) { let count: number = 1; let paras = options.parameters; for (let key in paras) { content['Parameters.' + count.toString() + '.ParameterKey'] = key; content['Parameters.' + count.toString() + '.ParameterValue'] = paras[key]; count++; } } const localStackInfo = await this.findStackInfo(stackName) if (localStackInfo.regionId) { regionId = localStackInfo.regionId } content['RegionId'] = regionId if (localStackInfo.stackId) { const stackInfo = await this.getStackByName(stackName, undefined, regionId) if (stackInfo !== null) { // update stack if (localStackInfo.stackId !== stackInfo.StackId) { error(`❌ Fail to update stack, because the locally recorded stackId(${localStackInfo.stackId}) and the remote stackId(${stackInfo.StackId}) are inconsistent.`) tmpSignal = 1; } else { content['StackId'] = stackInfo.StackId; let stackStatus = stackInfo.Status let stackUpdateTime = stackInfo.UpdateTime ? stackInfo.UpdateTime : stackInfo.CreateTime if (stackStatus.indexOf("IN_PROGRESS") == -1) { try { if (sync) { print('%s: deploying...', colors.bold(stackName)); } tmpSignal = await this.rosUpdateStack(client, content, requestOptions, outputs, skipIfNoChanges, stackUpdateTime, detailLog, sync, stackName, bucketName) } catch (e) { tmpSignal = 1; } } else { error('fail to update stack, because stack status is %s', stackStatus) tmpSignal = 1; } } } else { // create stack try { if (sync) { print('%s: deploying...', colors.bold(stackName)); } tmpSignal = await this.rosDeployStack(client, content, requestOptions, outputs, resourceGroupId, stackName, detailLog, sync, bucketName) } catch (e) { tmpSignal = 1; } } } else { const stackInfo = await this.getStackByName(stackName, resourceGroupId, regionId) if (stackInfo !== null) { // stack is exist send error message error('Fail to create stack, because stack %s already exists in this region.', stackName) tmpSignal = 1; } else { // create stack try { if (sync) { print('%s: deploying...', colors.bold(stackName)); } tmpSignal = await this.rosDeployStack(client, content, requestOptions, outputs, resourceGroupId, stackName, detailLog, sync, bucketName) } catch (e) { tmpSignal = 1; } } } exitSignal = exitSignal | tmpSignal; if (exitSignal == 1) { await this.deleteBucket(client, bucketName); } } exit(exitSignal) } public async diff(options: DiffOptions) { let stacks = await this.selectAllStacksForDefault(options.stackNames); const client = await this.getRosClient(); let regionInLocal = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); regionInLocal = regionInLocal ? regionInLocal : process.env.REGION_ID; const stream = options.stream || process.stderr; const contextLines = options.contextLines || 3; let exitSignal = 0; let requests = []; for (let stack of stacks.stackArtifacts) { let stackInfo = await this.findStackInfo(stack.id); let regionId = stackInfo.regionId ? stackInfo.regionId : regionInLocal; if (!stackInfo.stackId) { stream.write(format('Stack %s has not been deployed or stack doesn\'t exist in the stack.info.json file \n', colors.bold(stack.displayName))); continue; } requests.push(client.getTemplate({RegionId: regionId, StackId: stackInfo.stackId}, requestOptions) .then((res: any) => { const template = deserializeStructure(res.TemplateBody); stream.write(format('Stack %s\n', colors.bold(stack.displayName))); printStackDiff(template, stack, contextLines, stream); }, (ex: any) => { if (ex.code == 'StackNotFound') { warning(`\n ❌ The specific stack doesn't exit and it's local status will be set to synth.`); this.updateStackInfo(stack.id, SYNTH_STACK, undefined); } else { error('fail to get template: %s %s', ex.code, ex.message); } exitSignal = 1; })); } await Promise.all(requests) exit(exitSignal) } public async event(options: EventOptions) { await this.syncStackInfo(); let stacks = await this.selectOnlySingleStackForDefault(options.stackNames); const stackNames = stacks.stackIds let exitSignal = 0; const client = await this.getRosClient(); let LogicalResourceIds: string[] = []; if (options.logicalResourceId) { LogicalResourceIds.push(options.logicalResourceId) } let requests = []; for (let stackName of stackNames) { let stackInfo = await this.findStackInfo(stackName); let stackId = stackInfo.stackId let region = stackInfo.regionId; if (!region) { region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); region = region ? region : process.env.REGION_ID; } if (!stackId) { error(`The stack id of the specific stack(${stackName}) doesn't exist in the stack.info.json file.`) exitSignal = 1; continue } requests.push(client .listStackEvents({ StackId: stackId, RegionId: region, LogicalResourceId: LogicalResourceIds, PageSize: options.pageSize ? Number(options.pageSize) : 10, PageNumber: options.pageNumber ? Number(options.pageNumber) : 1 }, requestOptions) .then((res: any) => { success(`\n ✅ The events of the stack %s are: \n %s \n`, colors.blue(stackName), colors.blue(JSON.stringify(res.Events, null, "\t"))); }, (ex: any) => { if (ex.code == 'StackNotFound') { warning(`\n ❌ The specific stack doesn't exit and it's local status will be set to destroy.`); this.updateStackInfo(stackName, DESTROY_STACK, undefined); } else { error('fail to get stack events: %s %s', ex.code, ex.message) } exitSignal = 1; })); } await Promise.all(requests) exit(exitSignal); } public async output(options: OutPutOptions) { await this.syncStackInfo(); let stacks = await this.selectAllStacksForDefault(options.stackNames); const stackNames = stacks.stackIds const client = await this.getRosClient(); let exitSignal = 0; let requests = []; for (let stackName of stackNames) { let stackInfo = await this.findStackInfo(stackName); let stackId = stackInfo.stackId let region = stackInfo.regionId; if (!region) { region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); region = region ? region : process.env.REGION_ID; } if (!stackId) { error(`The specific stack doesn't exist in the stack.info.json file, Please check the accuracy of the stack: %s!`, stackName) exitSignal = 1; continue } requests.push(client .getStack({ StackId: stackId, RegionId: region }, requestOptions) .then((res: any) => { success(`\n ✅ The outputs of the stack %s are: \n %s \n`, colors.blue(stackName), colors.blue(JSON.stringify(res.Outputs, null, "\t"))); }, (ex: any) => { if (ex.code == 'StackNotFound') { warning(`\n ❌ The specific stack doesn't exit and it's local status will be set to destroy.`); this.updateStackInfo(stackName, DESTROY_STACK, undefined); } else { error('fail to get stack outputs: %s %s', ex.code, ex.message) } exitSignal = 1; })); } await Promise.all(requests) exit(exitSignal); } public async resource(options: ResourceOptions) { await this.syncStackInfo(); let stacks = await this.selectAllStacksForDefault(options.stackNames); let stackNames: string[] = []; for (let stack of stacks.stackArtifacts) { if ((await this.findStackInfo(stack.id)).stackId) { stackNames.push(stack.id); } else { error(`The specific stack doesn't exist in the stack.info.json file, Please check the accuracy of the stack: %s!`, stack.id) } } const client = await this.getRosClient(); let exitSignal = 0; let requests = []; for (let stackName of stackNames) { let stackInfo = await this.findStackInfo(stackName); let region = stackInfo.regionId; if (!region) { region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); region = region ? region : process.env.REGION_ID; } requests.push(client .listStackResources({ StackId: (await this.findStackInfo(stackName)).stackId, RegionId: region, }, requestOptions) .then((res: any) => { success(`\n ✅ The list of resources in the stack %s is: \n %s \n`, colors.blue(stackName), colors.blue(JSON.stringify(res.Resources, null, "\t"))); }, (ex: any) => { if (ex.code == 'StackNotFound') { warning(`\n ❌ The specific stack doesn't exit and it's local status will be set to destroy.`); this.updateStackInfo(stackName, DESTROY_STACK, undefined); } else { error('fail to get stack resource: %s %s', ex.code, ex.message) } exitSignal = 1; })); } await Promise.all(requests) exit(exitSignal); } public async generateStackInfo(options: GenerateStackInfoOptions) { let filePath = path.join(LOCAL_PATH + STACK_INFO); let stacks = await this.selectAllStacksForDefault([]); let stackNames: string[] = []; let StackInfos: { [key: string]: any } = {}; stackNames = stacks.stackIds for (let stackName of stackNames) { const stackInfo = await this.getStackByName(stackName, options.resourceGroupId, undefined) if (stackInfo !== null) { StackInfos[stackName] = { status: DEPLOY_STACK, stackId: stackInfo.StackId, regionId: stackInfo.RegionId } } else { StackInfos[stackName] = { status: INIT_STACK, stackId: null, regionId: null }; } } fs.writeFileSync(filePath, JSON.stringify(StackInfos, null, '\t')); success( `\n ✅ The generate stack info has completed!`, ); exit(0) } private async getStackByName(stackName: string, resourceGroupId: any, region: string | undefined) { const client = await this.getRosClient(); if (!region) { region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); region = region ? region : process.env.REGION_ID; } let params: { [name: string]: any } = { RegionId: region, PageSize: 10, PageNumber: 1, StackName: [stackName] }; if (resourceGroupId) { params['ResourceGroupId'] = resourceGroupId } try { const result = await client.listStacks(params, requestOptions) if (result.Stacks[0]) { return result.Stacks[0] } else { return null } } catch { return null } } public async listStacks(options: ListStackOptions) { await this.syncStackInfo(); const client = await this.getRosClient(); let params: any = {}; let region = options.region; params = { PageSize: options.pageSize ? Number(options.pageSize) : 10, PageNumber: options.pageNumber ? Number(options.pageNumber) : 1 }; if (options.resourceGroupId) { params.ResourceGroupId = options.resourceGroupId } let exitSignal = 0; let requests = []; if (!options.all) { let stacks = await this.selectAllStacksForDefault(options.stackNames); let regionMap: Map<string, string[]> = new Map<string, string[]>(); for (const stack of stacks.stackArtifacts) { let stackInfo = await this.findStackInfo(stack.id); if (region && stackInfo.regionId !== region) {continue} if (regionMap.has(stackInfo.regionId)) { regionMap.get(stackInfo.regionId)!.push(stack.id) } else { regionMap.set(stackInfo.regionId, [stack.id]) } } regionMap.forEach((stackNames, r) => { params.StackName = stackNames; params.RegionId = r; requests.push(client.listStacks(params, requestOptions) .then((res: any) => { success(`\n ✅ The Stacks list in ${r} is:\n ${colors.blue(JSON.stringify(res.Stacks, null, "\t"))} \n`); }, (ex: any) => { error('❌ Fail to list stacks in ${r}: %s %s', ex.code, ex.message) exitSignal = 1; })); }) } else { if (!region) { let config_region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); region = config_region ? config_region : process.env.REGION_ID; } params.RegionId = region; requests.push(client.listStacks(params, requestOptions) .then((res: any) => { success(`\n ✅ The Stacks list in ${region} is:\n ${colors.blue(JSON.stringify(res.Stacks, null, "\t"))} \n`); }, (ex: any) => { error('❌ Fail to list stacks in %s: %s %s', region, ex.code, ex.message) exitSignal = 1; })); } await Promise.all(requests) exit(exitSignal); } public async handleDel(client: any, name: string) { try { await client.delete(name); } catch (error) { return error; } } // 删除多个文件。 public async deleteProjects(client: any) { let list = await client.list(); list.objects = list.objects || []; while (list.objects.length > 0) { await Promise.all(list.objects.map((v: any) => this.handleDel(client, v.name))); list = await client.list(); list.objects = list.objects || []; } } public async deleteBucket(client: any, bucketName: string) { try { // 指定存储空间名称。 await this.deleteProjects(client); await client.deleteBucket(bucketName); console.log(`Delete bucket(${bucketName}) successfully!`); } catch (err) { if (err.code === 'NoSuchBucket') { return } error(err); } } public async destroy(options: DestroyOptions) { await this.syncStackInfo(); let stacks = await this.selectAllStacksForDefault(options.stackNames); let stackNames: string[] = []; let sync = options.sync for (let stack of stacks.stackArtifacts) { if ((await this.findStackInfo(stack.id)).stackId) { stackNames.push(stack.id); } } if (!options.quiet) { while (true) { let confirm = readlineSync.question( 'The following stack(s) will be destroyed(Only deployed stacks will be displayed).\n\n' + stackNames.toString() + '\n\nPlease confirm.(Y/N)\n', ); if (confirm === 'n' || confirm === 'N') { return; } if (confirm === 'y' || confirm === 'Y') { break; } } } let config_region = await CdkToolkit.getJson(CONFIG_NAME, 'regionId', true); config_region = config_region ? config_region : process.env.REGION_ID; const client = await this.getRosClient(); let exitSignal = 0; for (let stackName of stackNames) { let stackInfo = await this.findStackInfo(stackName); const bucketName = stackInfo.assetAssociatedBucketName; let content: { [name: string]: any } = { StackId: stackInfo.stackId, RegionId: stackInfo.regionId ? stackInfo.regionId : config_region, }; if (sync) { print('%s: destroying...', colors.bold(stackName)); exitSignal = await this.syncDestroyStack(client, content, requestOptions) } else { await client.deleteStack(content, requestOptions) .then((res: any) => { this.updateStackInfo(stackName, DESTROY_STACK, undefined); success(`\n ✅ Delete the stack(${stackName}) successfully!\nRequestedId: %s`, colors.blue(res.RequestId)); }, (ex: any) => { if (ex.code == 'StackNotFound') { warning(`\n ❌ The specific stack doesn't exit and it's local status will be set to destroy.`); this.updateStackInfo(stackName, DESTROY_STACK, undefined); } else { error('fail to delete stack: %s %s', ex.code, ex.message) exitSignal = 1; } }); } if (bucketName !== undefined && bucketName !== null && bucketName !== '') { const config = await CdkToolkit.getConfig(); let client; if (!config.accessKeyId || !config.accessKeySecret) { error("Please use 'ros-cdk config (-g)' or set environment to set your account configuration firstly!"); exit(1); } else if (!config.securityToken) { client = new ossClient({ region: `oss-${config.regionId}`, accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret, bucket: stackInfo.assetAssociatedBucketName }); } else { client = new ossClient({ region: `oss-${config.regionId}`, accessKeyId: config.accessKeyId, accessKeySecret: config.accessKeySecret, securityToken: config.securityToken, bucket: stackInfo.assetAssociatedBucketName }); } await this.deleteBucket(client, bucketName); } } exit(exitSignal); } private async syncStackInfo() { let filePath = path.join(LOCAL_PATH + STACK_INFO); let fileContent = {}; if (fs.existsSync(filePath)) { fileContent = fs.readFileSync(filePath).toString(); } else { fs.writeFileSync(filePath, JSON.stringify(fileContent, null, '\t')); } let temp: { [key: string]: any } = {}; // selector.length = 0 means select all stacks from this app const stacks = await this.selectAllStacksForDefault([]); for (const stack of stacks.stackArtifacts) { let value = await CdkToolkit.getJson(STACK_INFO, stack.id); // if the stack has no info, then add init tag for it if (value) { temp[stack.id] = value; } else { temp[stack.id] = { status: INIT_STACK, stackId: null, regionId: null }; } } fs.writeFileSync(filePath, JSON.stringify(temp, null, '\t')); } private async updateStackInfo(stackName: string, value: string, regionId: string|undefined, bucketName?: string, status?: string) { let filePath = path.join(LOCAL_PATH + STACK_INFO); let fileContent = fs.readFileSync(filePath).toString(); let info = JSON.parse(fileContent); let stackInfo = info[stackName]; if (!stackInfo) { stackInfo = info[stackName] = {}; } if (value.length === DEPLOY_STACK_ID_LENGTH) { stackInfo.status = status?? DEPLOY_STACK; stackInfo.stackId = value; stackInfo.regionId = regionId; stackInfo.assetAssociatedBucketName = bucketName; } else { stackInfo.status = value; if (value === DESTROY_STACK) { stackInfo.stackId = null; stackInfo.regionId = null; stackInfo.assetAssociatedBucketName = null; } } fs.writeFileSync(filePath, JSON.stringify(info, null, '\t')); } private async findStackInfo(stackName: string) { let filePath = path.join(LOCAL_PATH + STACK_INFO); let fileContent = fs.readFileSync(filePath).toString(); return JSON.parse(fileContent)[stackName]; } private async selectAllStacksForDefault(stackNames: string[]) { const assembly = await this.assembly(); const stacks = await assembly.selectStacks(stackNames, { defaultBehavior: DefaultSelection.AllStacks, }); // No validation return stacks; } private async selectOnlySingleStackForDefault(stackNames: string[]) { const assembly = await this.assembly(); const stacks = await assembly.selectStacks(stackNames, { defaultBehavior: DefaultSelection.OnlySingle, }); // No validation return stacks; } private async selectStacksForDeploy(stackNames: string[], exclusively?: boolean) { const assembly = await this.assembly(); const stacks = await assembly.selectStacks(stackNames, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, defaultBehavior: DefaultSelection.OnlySingle, }); await this.validateStacks(stacks); return stacks; } private async selectStacksForDiff(stackNames: string[], exclusively?: boolean) { const assembly = await this.assembly(); const stacks = await assembly.selectStacks(stackNames, { extend: exclusively ? ExtendedStackSelection.None : ExtendedStackSelection.Upstream, defaultBehavior: DefaultSelection.AllStacks, }); await this.validateStacks(stacks); return stacks; } /** * Validate the stacks for errors and warnings according to the CLI's current settings */ private async validateStacks(stacks: StackCollection) { stacks.processMetadataMessages({ ignoreErrors: this.props.ignoreErrors, strict: this.props.strict, verbose: this.props.verbose, }); } private assembly(): Promise<CloudAssembly> { return this.props.cloudExecutable.synthesize(); } private async getCliConfig(loadFilePath: string) { let filePath: string; let configureInfo: any = {}; let configureInfos: any = {}; let AKProfileList: object[] = []; let StsProfileList: object[] = []; let RamRoleArnProfileList: object[] = []; let EcsRamRoleProfileList: object[] = []; if (loadFilePath) { filePath = loadFilePath } else { let homePath = os.homedir(); filePath = homePath + CLI_CONFIG_FILE; } if (fs.existsSync(filePath)) { let file = fs.readFileSync(filePath).toString(); let AllProfiles = JSON.parse(file)['profiles']; for (let profile of AllProfiles) { configureInfo = { accessKeyId: profile.access_key_id, accessKeySecret: profile.access_key_secret, region: profile.region_id, securityToken: profile.sts_token, roleArn: profile.ram_role_arn, roleSessionName: profile.ram_session_name, roleName: profile.ram_role_name, name: profile.name } if (profile.mode === 'AK') { AKProfileList.push(configureInfo) configureInfos.AK = AKProfileList } else if (profile.mode === 'StsToken') { StsProfileList.push(configureInfo) configureInfos.StsToken = StsProfileList } else if (profile.mode === 'RamRoleArn') { RamRoleArnProfileList.push(configureInfo) configureInfos.RamRoleArn = RamRoleArnProfileList } else if (profile.mode === 'EcsRamRole') { EcsRamRoleProfileList.push(configureInfo) configureInfos.EcsRamRole = EcsRamRoleProfileList } } if (!configureInfos || Object.keys(configureInfos).length == 0) { error("WANRNING: Please check the accuracy of the mode and profile configuration entered."); exit(1); } return configureInfos } else { error("WANRNING: Please check Aliyun Cli tool configure accuracy of the default path or specified path."); exit(1); } } private async rosDeployStack(client: any, content: any, requestOptions: any, outputsFile: boolean, resourceGroupId: any, stackName: any, detailLog: boolean, sync: boolean, bucketName?: string) { const stackOutputs: { [key: string]: any } = {}; let sleepTime = 0; let stackId: any; for (let i = 0; i < 10; i++) { try { let createStackResult = await client.createStack(content, requestOptions) stackId = createStackResult.StackId; break; } catch (e) { if (detailLog) { error(`The ${i}th deployment attempt failed, as detailed in ${e}`); } if (!e.data || !("RequestId" in e.data) || e.code === 'ServiceUnavailable') { if (sleepTime < 20000) { sleepTime = sleepTime + 5000; } await sleep(sleepTime); } else if (e.code === 'LastTokenProcessing') { const newStackIdInfo = await this.getStackByName(stackName, resourceGroupId, content['RegionId']) if (newStackIdInfo) { stackId = newStackIdInfo.StackId; } break } else { error('❌ Fail to create stack: ErrorCode: %s\nRequestedId: %s\nErrorMessage:%s', e.code, e.data["RequestId"], e.message) throw e; } } } if (!stackId) { error('❌ Fail to create stack, please check your service endpoint.') throw new NetworkError('An unknown network error occurs, please check the endpoint and try again later.'); } if (sync) { const block = new RewritableBlock(stream); withDefaultPrinterObj = setInterval(async function () { await CdkToolkit.withDefaultPrinter(client, content, requestOptions, stackId, block, 'deploy') }, 5000); while (true) { try { await sleep(1000) let params = { RegionId: content['RegionId'], StackId: stackId }; const getStackResult = await client.getStack(params, requestOptions) const status = getStackResult.Status const statusReason = getStackResult.StatusReason const stackName = getStackResult.StackName const outputs = getStackResult.Outputs const regComplete = RegExp(/COMPLETE/) const regFailed = RegExp(/FAILED/) if (regComplete.exec(status) || regFailed.exec(status)) { clearInterval(withDefaultPrinterObj); await CdkToolkit.withDefaultPrinter(client, content, requestOptions, stackId, block, 'deploy') if (outputs !== undefined) { print('\nOutputs:'); stackOutputs[stackName] = outputs; for (const output of outputs) { const value = output['OutputValue']; const key = output['OutputKey']; const description = output['Description']; print('\n Key: %s Value: %s Description: %s', colors.cyan(key), colors.cyan(value), colors.cyan(description)); } if (outputsFile) { fs.writeFileSync(path.join(LOCAL_PATH + OUTPUTS_JSON), JSON.stringify(stackOutputs, null, '\t')); } } if (status.toString() == 'CREATE_COMPLETE') { await this.updateStackInfo(stackName, stackId, content['RegionId'], bucketName); success( `\n ✅ The deployment(sync create stack) has completed!\nStatus: %s\nStatusReason: %s\nStackId: %s`, colors.blue(status), colors.blue(statusReason), colors.blue(stackId) ); return 0; } else { await this.updateStackInfo(stackName, stackId, content['RegionId'], bucketName, status.toString()); error(`\n❌ Fail to sync create stack:\nStatus: %s\nStatusReason: %s\nStackId: %s`, colors.blue(status), colors.blue(statusReason), colors.blue(stackId) ); return 1; } } } catch (e) { if (detailLog) { error('An error occurs trying to get the resource stack details: %s', e); } if (e.code == 'Throttling.User' || e.code == 'Throttling' || e.code == 'Throttling.API') { await sleep(30000) } else { error('❌ Fail to sync create stack: ErrorCode: %s\nErrorMessage:%s', e.code, e.message) clearInterval(withDefaultPrinterObj); throw e; } } } } else { await this.updateStackInfo(stackName, stackId, content['RegionId'], bucketName); success( `\n ✅ The deployment(create stack) has completed!\nStackId: %s`, colors.blue(stackId) ); return 0; } } private static async getResources(client: any, content: any, requestOptions: any, stackId: any) { try { const listStackResourcesResult = await client.listStackResources({ StackId: stackId, RegionId: content['RegionId'], }, requestOptions) return listStackResourcesResult.Resources } catch (e) { error('fail to get new stack resource: %s %s', e.code, e.message) throw e; } } private static async withDefaultPrinter(client: any, content: any, requestOptions: any, stackId: any, block: any, action: string) { const lines = new Array<string>(); const resources = await CdkToolkit.getResources(client, content, requestOptions, stackId) if (action !== 'destroy') { for (let resource of resources) { lines.push(util.format(colors.blue('|%s |%s | %s | %s | %s | %s') + '\n', padRight(23, resource.StackName), padLeft(12, resource.CreateTime), padRight(20, resource.Status), padRight(23, resource.ResourceType), shorten(40, resource.PhysicalResourceId), resource.LogicalResourceId)); } } else { for (let resource of resources) { lines.push(util.format(colors.blue('|%s | %s | %s | %s | %s') + '\n', padRight(23, resource.StackName), padRight(20, resource.Status), padRight(23, resource.ResourceType), shorten(40, resource.PhysicalResourceId), resource.LogicalResourceId)); } } block.displayLines(lines) } private async rosUpdateStack(client: any, content: any, requestOptions: any, outputsFile: boolean, skipIfNoChanges: boolean, stackUpdateTime: any, detailLog: boolean, sync: boolean, stackName: string, bucketName?: string) { let sleepTime = 0; const stackId = content['StackId']; for (let i = 0; i < 10; i++) { try { await client.updateStack(content, requestOptions) break; } catch (e) { if (detailLog) { error(`The ${i}th update attempt failed, as detailed in ${e}`); } if (!e.data || !("RequestId" in e.data) || e.code === 'ServiceUnavailable') { if (sleepTime < 20000) { sleepTime = sleepTime + 5000; } await sleep(sleepTime); } else if (e.code === 'LastTokenProcessing') { break } else if (e.code == 'NotSupported' && e.message.startsWith('Update the completely same stack') && (skipIfNoChanges || bucketName !== undefined)) { success('The stack is completely the same, there is no need to update.') return 0 } else { error('❌ Fail to update stack: ErrorCode: %s\nRequestedId: %s\nErrorMessage:%s', e.code, e.data["RequestId"], e.message) throw e; } } } if (sync) { let params = { RegionId: content['RegionId'], StackId: stackId }; const stackOutputs: { [key: string]: any } = {}; // Wait for the stack state to change after updating it await sleep(5000); const block = new RewritableBlock(stream); withDefaultPrinterObj = setInterval(async function () { await CdkToolkit.withDefaultPrinter(client, content, requestOptions, stackId, block, 'update') }, 5000); while (true) { try { await sleep(1000) const getNewStackResult = await client.getStack(params, requestOptions) const status = getNewStackResult.Status const statusReason = getNewStackResult.StatusReason const outputs = getNewStackResult.Outputs const newUpdateTime = getNewStackResult.UpdateTime ? getNewStackResult.UpdateTime : "" if (newUpdateTime == stackUpdateTime) { // stack update in progress or update did not begin continue } const regComplete = RegExp(/COMPLETE/) const regFailed = RegExp(/FAILED/) if (regComplete.exec(status) || regFailed.exec(status)) { clearInterval(withDefaultPrinterObj); await CdkToolkit.withDefaultPrinter(client, content, requestOptions, stackId, block, 'update') if (outputs !== undefined) { print('\nOutputs:'); stackOutputs[stackName] = outputs; for (const output of outputs) { const value = output['OutputValue']; const key = output['OutputKey']; const description = output['Description']; print('\n Key: %s Value: %s Description: %s', colors.cyan(key), colors.cyan(value), colors.cyan(description)); } if (outputsFile) { fs.writeFileSync(path.join(LOCAL_PATH + OUTPUTS_JSON), JSON.stringify(stackOutputs, null, '\t')); } } if (status.toString() == 'UPDATE_COMPLETE') { await this.updateStackInfo(stackName, stackId, content['RegionId'], bucketName); success( `\n ✅ The deployment(sync update stack) has completed!\nStatus: %s\nStatusReason: %s\nStackId: %s`, colors.blue(status), colors.blue(statusReason), colors.blue(stackId) ); return 0; } else { await this.updateStackInfo(stackName, stackId, content['RegionId'], bucketName, status.toString()); error(`\n❌ Fail to sync update stack:\nStatus: %s\nStatusReason: %s\nStackId: %s`, colors.blue(status), colors.blue(statusReason), colors.blue(stackId) ); return 1; } } } catch (e) { if (detailLog) { error('An error occurs trying to get the resource stack details: %s', e); } if (e.code == 'Throttling.User' || e.code == 'Throttling' || e.code == 'Throttling.API') { await sleep(30000) } else { error('❌ Fail to sync update stack: ErrorCode: %s\nErrorMessage:%s', e.code, e.message) clearInterval(withDefaultPrinterObj); throw e; } } } } else { await this.updateStackInfo(stackName, stackId, content['RegionId'], bucketName); success( `\n ✅ The deployment(update stack) has completed!\nStackId: %s`, colors.blue(stackId), ); return 0; } } private async syncDestroyStack(client: any, content: any, requestOptions: any) { try { await client.deleteStack(content, requestOptions) const block = new RewritableBlock(stream); withDefaultPrinterObj = setInterval(async function () { await CdkToolkit.withDefaultPrinter(client, content, requestOptions, content['StackId'], block, 'destroy') }, 5000); while (true) { try { await sleep(1000) let params = { RegionId: content['RegionId'], StackId: content['StackId'] }; const getStackResult = await client.getStack(params, requestOptions) const status = getStackResult.Status const statusReason = getStackResult.StatusReason const stackName = getStackResult.StackName const regComplete = RegExp(/COMPLETE/) const regFailed = RegExp(/FAILED/) if (regComplete.exec(status) || regFailed.exec(status)) { clearInterval(withDefaultPrinterObj); await CdkToolkit.withDefaultPrinter(client, content, requestOptions, content['StackId'], block, 'destroy') success( `\n ✅ The task(sync destroy stack) has finished!\nstatus: %s\nStatusReason: %s\nStackId: %s`, colors.blue(status), colors.blue(statusReason), colors.blue(getStackResult.StackId) ); await this.updateStackInfo(stackName, DESTROY_STACK, undefined); if (regComplete.exec(status)) { return 0 } else { return 1 } } } catch (e) { if (e.code == 'Throttling.User' || e.code == 'Throttling' || e.code == 'Throttling.API') { await sleep(30000) } else { error('fail to sync destroy stack: %s %s', e.code, e.message) clearInterval(withDefaultPrinterObj); return 1 } } } } catch (e) { error('fail to sync destroy stack: %s %s', e.code, e.message) clearInterval(withDefaultPrinterObj); return 1 } } } export interface DiffOptions { stackNames: string[]; path: string; stream?: NodeJS.WritableStream; contextLines: number; } export interface DeployOptions { stackNames: string[]; /** * Only select the given stack * * @default false */ exclusively?: boolean; parameters?: { [name: string]: string | undefined }; timeout: string; sync: boolean; regionId: string; outputsFile: boolean; skipIfNoChanges: boolean; disableRollback: boolean; resourceGroupId: string; detailLog: boolean; } export interface DestroyOptions { stackNames: string[]; quiet?: boolean; sync: boolean; } export interface EventOptions { stackNames: string[]; logicalResourceId: string; pageNumber: string; pageSize: string; } export interface OutPutOptions { stackNames: string[]; } export interface ResourceOptions { stackNames: string[]; } export interface GenerateStackInfoOptions { resourceGroupId: string; } export interface ListStackOptions { stackNames: string[]; pageNumber: string; pageSize: string; all: string; resourceGroupId: string; region: string; } export interface ConfigSetOptions { global: string; endpoint: string; region: string; mode: string; ak: string; sk: string; sts: string; ramRoleArn: string; roleSessionName: string; ramRoleName: string; } export interface LoadConfigOptions { global?: string; loadFilePath: string; } export interface Tag { readonly Key: string; readonly Value: string; } export function writeAndUpdateLanguageInfo(language: string) { let filePath = path.join(LOCAL_PATH + PROJECT_CONFIG); let fileContent: any; if (fs.existsSync(filePath)) { fileContent = fs.readFileSync(filePath).toString(); let info = JSON.parse(fileContent); info['languageInfo'] = language; fs.writeFileSync(filePath, JSON.stringify(info, null, '\t')); } } export function readLanguageInfo() { let filePath = path.join(LOCAL_PATH + PROJECT_CONFIG); if (fs.existsSync(filePath)) { let fileContent = fs.readFileSync(filePath).toString(); return JSON.parse(fileContent)['languageInfo'].toString(); } else { return '' } } export function padLeft(n: number, x: string): string { if (x) { return ' '.repeat(Math.max(0, n - x.length)) + x; } else { return '' } } export function padRight(n: number, x: string): string { if (x) { return x + ' '.repeat(Math.max(0, n - x.length)); } else { return '' } } export function shorten(maxWidth: number, p: string) { if (p.length <= maxWidth) { return p; } const half = Math.floor((maxWidth - 3) / 2); return p.substr(0, half) + '...' + p.substr(p.length - half); } export function desensitization(inputString: string, mixLength = 3) { // mixLength 字符串少于一定值则脱敏全部,增加脱敏位数 if (isString(inputString) || isNumber(inputString)) { const str = String(inputString); if (str.length <= mixLength) { return '*'.repeat(mixLength); } const len = str.length; const firstStr = str.substr(0, str.length / mixLength); const lastStr = str.substr(-str.length / mixLength); const middleStr = str .substring(str.length / mixLength, len - Math.abs(-str.length / mixLength)) .replace(/[\s\S]/gi, '*'); return firstStr + middleStr + lastStr; } return ''; }