packages/cli/lib/mdaa-cli.ts (803 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { ConfigurationElement, MdaaConfigParamRefValueTransformerProps, MdaaConfigRefValueTransformer, MdaaCustomAspect, MdaaCustomNaming, TagElement, Workspace, } from '@aws-mdaa/config'; import * as fs from 'fs'; import * as path from 'path'; import { MdaaCliConfig, MdaaDomainConfig, MdaaEnvironmentConfig, MdaaModuleConfig, TerraformConfig, } from './mdaa-cli-config-parser'; import { DomainEffectiveConfig, EffectiveConfig, EnvEffectiveConfig, ModuleDeploymentConfig, ModuleEffectiveConfig, } from './config-types'; import { generateContextCdkParams } from './utils'; export interface DeployStageMap { [key: string]: ModuleDeploymentConfig[]; } export class MdaaDeploy { private readonly config: MdaaCliConfig; private readonly action: string; private readonly cwd: string; private readonly domainFilter?: string[]; private readonly envFilter?: string[]; private readonly moduleFilter?: string[]; private readonly npmTag?: string; private readonly roleArn?: string; private readonly workingDir: string; private readonly mdaaVersion?: string; private readonly npmDebug: boolean; private readonly updateCache: { [prefix: string]: boolean } = {}; private readonly devopsMode?: boolean; private static readonly DEFAULT_DEPLOY_STAGE = '1'; private readonly localPackages: { [packageName: string]: string }; private readonly cdkPushdown?: string[]; private readonly cdkVerbose?: boolean; private readonly testMode: boolean; private readonly noFail: boolean; private static readonly TF_ACTION_MAPPINGS: { [key: string]: string } = { list: 'validate', ls: 'validate', synth: 'validate', diff: 'plan', deploy: 'apply', destroy: 'destroy', }; constructor(options: { [key: string]: string }, cdkPushdown?: string[], configContents?: ConfigurationElement) { this.action = options['action']; /* istanbul ignore next */ if (!this.action) { throw new Error('MDAA action must be specified on command line: mdaa <action>'); } this.noFail = this.booleanOption(options, 'nofail'); this.testMode = this.booleanOption(options, 'testing'); this.cwd = process.cwd(); this.mdaaVersion = options['mdaa_version']; this.domainFilter = options['domain']?.split(',').map(x => x.trim()); this.envFilter = options['env']?.split(',').map(x => x.trim()); this.moduleFilter = options['module']?.split(',').map(x => x.trim()); this.roleArn = options['role_arn']; this.npmTag = options['tag']; // nosemgrep this.workingDir = options['working_dir'] ? path.resolve(options['working_dir']) : path.resolve('./.mdaa_working'); console.log(`Set MDAA working directory to ${this.workingDir}`); this.npmDebug = this.booleanOption(options, 'npm_debug'); this.devopsMode = this.booleanOption(options, 'devops'); this.cdkPushdown = cdkPushdown; this.cdkVerbose = this.booleanOption(options, 'cdk_verbose'); const configFileName = options['config'] ?? './mdaa.yaml'; this.config = this.loadConfig(configFileName, configContents); if (options['local_mode']) { console.log('Use of -l flag no longer necessary. Execution mode is automatically determined.'); } /* istanbul ignore next */ if (options['clear']) { console.log(`Removing all previously installed Node.JS packages from ${this.workingDir}/nodejs`); this.execCmd(`rm -rf '${this.workingDir}/nodejs'`); console.log(`Removing all previously installed Python packages from ${this.workingDir}/python`); this.execCmd(`rm -rf '${this.workingDir}/python'`); } this.localPackages = this.loadLocalPackages(); if (this.devopsMode) { console.log('Running MDAA in devops mode.'); } this.installPython(); } private installPython() { const commandExists = require('command-exists'); const pipCommandExists = commandExists.sync('pip'); if (pipCommandExists) { const pipCmd = `pip install --upgrade -q -r ${__dirname}/../requirements.txt -t ${this.workingDir}/python`; console.log(`Found pip. Installing python with cmd: ${pipCmd}`); this.execCmd(pipCmd); } else { throw new Error('pip not availalable'); } } private booleanOption(options: { [key: string]: string }, name: string): boolean { return !!options[name]; } private loadConfig(configFileName: string, configContents: ConfigurationElement | undefined): MdaaCliConfig { if (configContents) { return new MdaaCliConfig({ configContents: configContents }); } else { if (!fs.existsSync(configFileName)) { if (configFileName == './mdaa.yaml') { if (fs.existsSync('./caef.yaml')) { console.warn("Default config file found at 'caef.yaml'."); return new MdaaCliConfig({ filename: './caef.yaml' }); } else { throw new Error("Cannot open default config file at 'mdaa.yaml' or 'caef.yaml'"); } } else { throw new Error(`Cannot open config file at ${configFileName}`); } } else { return new MdaaCliConfig({ filename: configFileName }); } } } private loadLocalPackages() { // nosemgrep const workspaceQueryJson = require('child_process') .execSync(`npm query .workspace --prefix '${__dirname}/../../../'`) .toString(); // ISSUE-499 needs validation on the edge. We can't guarantee `Workspace` is what we are getting const workspace: Workspace[] = JSON.parse(workspaceQueryJson); const localPackages = Object.fromEntries( workspace .filter(pkgInfo => { return pkgInfo['location'].startsWith('packages/apps/'); }) .map(pkgInfo => { // nosemgrep return [`${pkgInfo['name']}`, path.resolve(`${__dirname}/../../../${pkgInfo['location']}`)]; }), ); /* istanbul ignore next */ if (Object.entries(localPackages).length > 0) { console.log(`Loaded ${Object.entries(localPackages).length} MDAA modules from local codebase.`); } return localPackages; } public deploy() { const globalEffectiveConfig: EffectiveConfig = { effectiveContext: this.config.contents.context || {}, effectiveTagConfig: this.config.contents.tag_config_data || {}, tagConfigFiles: this.config.contents.tag_configs || [], effectiveMdaaVersion: this.config.contents.mdaa_version || this.mdaaVersion, customAspects: this.config.contents.custom_aspects || [], customNaming: this.config.contents.naming_module && this.config.contents.naming_class ? { naming_module: this.config.contents.naming_module, naming_class: this.config.contents.naming_class, naming_props: this.config.contents.naming_props, } : undefined, envTemplates: this.config.contents.env_templates || {}, terraform: this.config.contents.terraform, }; this.deployDomains(globalEffectiveConfig); if (this.devopsMode) { this.deployDevOps(globalEffectiveConfig); } } private deployDevOps(effectiveConfig: EffectiveConfig) { const devopsModuleConfig: ModuleEffectiveConfig = { ...effectiveConfig, modulePath: '@aws-mdaa/devops', moduleName: 'devops', useBootstrap: false, envName: 'multi-envs', domainName: 'multi-domains', effectiveModuleConfig: (this.config.contents.devops || {}) as ConfigurationElement, }; const devOpsModuleDeploymentConfig = this.prepCdkModule(devopsModuleConfig); this.deployModule(devOpsModuleDeploymentConfig); } private deployDomains(globalEffectiveConfig: EffectiveConfig) { if (this.domainFilter && !this.devopsMode) { console.log(`Filtering for domain(s) ${this.domainFilter}`); } Object.keys(this.config.contents.domains) .filter( domainName => this.devopsMode || this.domainFilter == undefined || this.domainFilter?.includes(domainName), ) .forEach(domainName => { const domain = this.config.contents.domains[domainName]; const domainEffectiveConfig: DomainEffectiveConfig = this.computeDomainEffectiveConfig( domainName, domain, globalEffectiveConfig, ); this.deployDomain(domain, domainEffectiveConfig); }); } public deployDomain(domain: MdaaDomainConfig, domainEffectiveConfig: DomainEffectiveConfig) { if (!this.devopsMode) { console.log(`-----------------------------------------------------------`); console.log(`Domain ${domainEffectiveConfig.domainName}: Running ${this.action}`); console.log(`-----------------------------------------------------------`); } if (this.envFilter && !this.devopsMode) { console.log(`Domain ${domainEffectiveConfig.domainName}: Filtering for env ${this.envFilter}`); } Object.keys(domain.environments) .filter(envName => this.devopsMode || this.envFilter == undefined || this.envFilter?.includes(envName)) .forEach(envName => { const env = domain.environments[envName]; if ( env.template && (!domainEffectiveConfig.envTemplates || !domainEffectiveConfig.envTemplates[env.template]) ) { throw new Error(`Environment "${envName}" references invalid template name: ${env.template}.`); } const template = env.template && domainEffectiveConfig.envTemplates ? domainEffectiveConfig.envTemplates[env.template] : {}; // nosemgrep const _ = require('lodash'); const envMergedConfig = _.mergeWith(env, template); const envEffectiveConfig: EnvEffectiveConfig = this.computeEnvEffectiveConfig( envName, envMergedConfig, domainEffectiveConfig, ); this.deployEnv(envMergedConfig, envEffectiveConfig); }); } private deployEnv(env: MdaaEnvironmentConfig, envEffectiveConfig: EnvEffectiveConfig) { if (!env.modules) { throw new Error(`Cannot deploy environment "${envEffectiveConfig.envName}" with no modules.`); } if (this.moduleFilter && !this.devopsMode) { console.log( `Env ${envEffectiveConfig.domainName}/${envEffectiveConfig.envName}: Filtering for module ${this.moduleFilter}`, ); } if (envEffectiveConfig.useBootstrap) { env.modules['caef-bootstrap'] = { module_path: '@aws-mdaa/bootstrap', }; } const moduleEffectiveConfigs = Object.entries(env.modules).map(entry => { return this.computeModuleEffectiveConfig(entry[0], entry[1], envEffectiveConfig); }); if (!this.devopsMode) { this.deployEnvModules(envEffectiveConfig, moduleEffectiveConfigs); } else { moduleEffectiveConfigs.forEach(config => { this.testModuleEffectiveConfigForPipelines(config); }); } } private testModuleEffectiveConfigForPipelines(moduleEffectiveConfig: ModuleEffectiveConfig) { const pipelines = Object.entries(this.config.contents.devops?.pipelines || {}) .filter(pipelineEntry => { const pipelineConfig = pipelineEntry[1]; return ( (pipelineConfig.domainFilter == undefined || pipelineConfig.domainFilter?.includes(moduleEffectiveConfig.domainName)) && (pipelineConfig.envFilter == undefined || pipelineConfig.envFilter?.includes(moduleEffectiveConfig.envName)) && (pipelineConfig.moduleFilter == undefined || pipelineConfig.moduleFilter?.includes(moduleEffectiveConfig.moduleName)) ); }) .map(entry => entry[0]); if (pipelines.length == 1) { console.log( `Module ${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${moduleEffectiveConfig.moduleName} will be deployed via pipeline ${pipelines[0]}`, ); } else if (pipelines.length > 1) { throw new Error( `Module ${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${moduleEffectiveConfig.moduleName} matches multiple pipeline filters: ${pipelines}`, ); } else { console.warn( `WARNING: Module ${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${moduleEffectiveConfig.moduleName} matches no pipeline filters`, ); } } private deployEnvModules(envEffectiveConfig: EnvEffectiveConfig, moduleEffectiveConfigs: ModuleEffectiveConfig[]) { console.log(`-----------------------------------------------------------`); console.log( `Env ${envEffectiveConfig.domainName}/${envEffectiveConfig.envName}: Prepping Modules and Computing Stages`, ); console.log(`-----------------------------------------------------------`); const envDeployStages: DeployStageMap = this.computeEnvDeployStages(moduleEffectiveConfigs); if (!this.devopsMode) { console.log(`-----------------------------------------------------------`); console.log(`Env ${envEffectiveConfig.domainName}/${envEffectiveConfig.envName}: Running ${this.action}`); console.log(`-----------------------------------------------------------`); } const orderedStages = this.action == 'destroy' ? Object.keys(envDeployStages) .sort((a, b) => +a - +b) .reverse() : Object.keys(envDeployStages).sort((a, b) => +a - +b); orderedStages.forEach(stage => { console.log(`Env ${envEffectiveConfig.domainName}/${envEffectiveConfig.envName} Running MDAA stage ${stage}`); const stageApps = envDeployStages[stage]; stageApps.forEach(module => { this.deployModule(module); }); }); } private computeEnvDeployStages(moduleEffectiveConfigs: ModuleEffectiveConfig[]): DeployStageMap { const deployStages: DeployStageMap = {}; moduleEffectiveConfigs .filter( moduleEffectiveConfig => this.devopsMode || this.moduleFilter == undefined || this.moduleFilter?.includes(moduleEffectiveConfig.moduleName), ) .forEach(moduleEffectiveConfig => { const logPrefix = `${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${moduleEffectiveConfig.moduleName}`; console.log(`Module ${logPrefix}: Prepping packages`); const moduleDeploymentConfig = this.prepModule(moduleEffectiveConfig); const customNamingModulePath = moduleEffectiveConfig.customNaming && moduleEffectiveConfig.customNaming.naming_module.startsWith('@') ? this.prepNpmPackage(logPrefix, moduleEffectiveConfig.customNaming.naming_module) : moduleEffectiveConfig.customNaming?.naming_module; const installedCustomNamingModule: MdaaCustomNaming | undefined = customNamingModulePath ? { naming_module: `${customNamingModulePath}`, naming_class: moduleEffectiveConfig.customNaming?.naming_class || '', naming_props: moduleEffectiveConfig.customNaming?.naming_props, } : undefined; const installedCustomAspects: MdaaCustomAspect[] = moduleEffectiveConfig.customAspects?.map(customAspect => { const [customAspectPath] = customAspect.aspect_module.startsWith('@') ? this.prepNpmPackage(logPrefix, customAspect.aspect_module) : [customAspect.aspect_module, true]; return { aspect_module: customAspectPath, aspect_class: customAspect.aspect_class, aspect_props: customAspect.aspect_props, }; }); const installedModuleConfig: ModuleDeploymentConfig = { ...moduleDeploymentConfig, customAspects: installedCustomAspects, customNaming: installedCustomNamingModule, }; const deployStage = this.computeModuleDeployStage(installedModuleConfig); if (deployStages[deployStage]) { deployStages[deployStage].push(installedModuleConfig); } else { deployStages[deployStage] = [installedModuleConfig]; } }); return deployStages; } private prepModule(moduleConfig: ModuleEffectiveConfig): ModuleDeploymentConfig { if (!moduleConfig.moduleType || moduleConfig.moduleType == 'cdk') { return this.prepCdkModule(moduleConfig); } else if (moduleConfig.moduleType == 'tf') { return this.prepTerraformModule(moduleConfig); } else { throw new Error(`Unknown module type: ${moduleConfig.moduleType}`); } } private createModuleTfWorkingConfig(moduleConfig: ModuleEffectiveConfig): ModuleEffectiveConfig { const moduleWorkingDir = path.resolve( `${this.workingDir}/terraform/${moduleConfig.domainName}/${moduleConfig.envName}/${moduleConfig.moduleName}`, ); this.execCmd(`mkdir -p '${moduleWorkingDir}'`); this.execCmd(`cp -r ${path.resolve(moduleConfig.modulePath)}/* ${moduleWorkingDir}`); return { ...moduleConfig, modulePath: moduleWorkingDir, }; } private prepTerraformModule(moduleConfig: ModuleEffectiveConfig): ModuleDeploymentConfig { if (!moduleConfig.modulePath) { throw new Error("module_path must be specified if module_type is 'tf'"); } if (!fs.existsSync(`${this.workingDir}/python/bin/checkov`) && !this.testMode) { console.log('Cannot locate checkov on path. Terraform modules cannot deploy. Check Python/Pip installation.'); process.exit(1); } const modulePath = path.resolve(moduleConfig.modulePath); console.log( `Module ${moduleConfig.domainName}/${moduleConfig.envName}/${moduleConfig.moduleName}: Resolved path to: ${modulePath}`, ); const preppedModuleConfig: ModuleEffectiveConfig = { ...moduleConfig, modulePath: modulePath, mdaaCompliant: moduleConfig.modulePath.startsWith('aws-mdaa') ? true : moduleConfig.mdaaCompliant, }; const moduleWorkingConfig = this.createModuleTfWorkingConfig(preppedModuleConfig); return { ...moduleWorkingConfig, moduleCmds: this.createTerraformCommands(moduleWorkingConfig), localModule: true, }; } private createTerraformCommands(moduleConfig: ModuleEffectiveConfig): string[] { const tfAction = MdaaDeploy.TF_ACTION_MAPPINGS[this.action] ?? this.action; this.createTerraformOverride(moduleConfig); const tfCmds: string[] = []; if (this.config.contents.region && this.config.contents.region.toLowerCase() != 'default') { tfCmds.push(`export AWS_DEFAULT_REGION=${this.config.contents.region}`); } tfCmds.push(`terraform init `); const checkovCmd: string[] = [ `export PYTHONPATH=${this.workingDir}/python && ${this.workingDir}/python/bin/checkov -d ${moduleConfig.modulePath}`, ]; checkovCmd.push('--summary-position bottom'); checkovCmd.push('--quiet'); checkovCmd.push('--compact'); checkovCmd.push('--download-external-modules true'); tfCmds.push(checkovCmd.join(' \\\n\t')); if (tfAction == 'plan') { const tfPlanCmd: string[] = []; if (this.config.contents.region && this.config.contents.region.toLowerCase() != 'default') { tfPlanCmd.push(`export AWS_DEFAULT_REGION=${this.config.contents.region}`); } tfPlanCmd.push('terraform plan'); tfPlanCmd.push(...this.createTerraformPlanApplyCmdArgs(moduleConfig)); tfPlanCmd.push(`--out ${moduleConfig.modulePath}/tfplan.binary`); tfCmds.push(tfPlanCmd.join(' \\\n\t')); } else if (tfAction == 'apply') { const tfApplyCmd: string[] = []; if (this.config.contents.region && this.config.contents.region.toLowerCase() != 'default') { tfApplyCmd.push(`export AWS_DEFAULT_REGION=${this.config.contents.region}`); } tfApplyCmd.push('terraform apply'); tfApplyCmd.push('-auto-approve'); tfApplyCmd.push(...this.createTerraformPlanApplyCmdArgs(moduleConfig)); tfCmds.push(tfApplyCmd.join(' \\\n\t')); } else { const tfCmd: string[] = []; if (this.config.contents.region && this.config.contents.region.toLowerCase() != 'default') { tfCmd.push(`export AWS_DEFAULT_REGION=${this.config.contents.region}`); } tfCmd.push(`terraform ${tfAction}`); tfCmds.push(tfCmd.join(' \\\n\t')); } return tfCmds; } private createTerraformPlanApplyCmdArgs(moduleConfig: ModuleEffectiveConfig): string[] { const tfCmd: string[] = []; tfCmd.push('-input=false'); if (moduleConfig.mdaaCompliant == undefined || moduleConfig.mdaaCompliant) { tfCmd.push(`-var org="${this.config.contents.organization}"`); tfCmd.push(`-var domain="${moduleConfig.domainName}"`); tfCmd.push(`-var env="${moduleConfig.envName}"`); tfCmd.push(`-var module_name="${moduleConfig.moduleName}"`); if (this.config.contents.region && this.config.contents.region.toLowerCase() != 'default') { tfCmd.push(`-var region="${this.config.contents.region}"`); } else { tfCmd.push('-var region="${AWS_DEFAULT_REGION}"'); } } const transformRefsProps: MdaaConfigParamRefValueTransformerProps = { org: this.config.contents.organization, domain: moduleConfig.domainName, env: moduleConfig.envName, module_name: moduleConfig.moduleName, context: moduleConfig.effectiveContext, }; const refsTransformer = new MdaaConfigRefValueTransformer(transformRefsProps); Object.entries(moduleConfig.effectiveModuleConfig).forEach(configEntry => { tfCmd.push( `-var ${configEntry[0]}="${JSON.stringify( // ISSUE-499 see if there is a guarantee that `configEntry` value is a string JSON.stringify(refsTransformer.transformValue(configEntry[1] as string)), )}"`, ); }); return tfCmd; } private createTerraformOverride(moduleConfig: ModuleEffectiveConfig) { if (moduleConfig.terraform?.override) { this.execCmd(`rm -rf ${moduleConfig.modulePath}/mdaa_override.tf.json `); const mdaaTfOverride = moduleConfig.terraform?.override || {}; if (mdaaTfOverride.terraform?.backend?.s3) { mdaaTfOverride.terraform.backend.s3 = { ...mdaaTfOverride.terraform?.backend?.s3, encrypt: true, key: `${this.config.contents.organization}-${moduleConfig.domainName}-${moduleConfig.envName}-${moduleConfig.moduleName}`, }; } fs.writeFileSync(`${moduleConfig.modulePath}/mdaa_override.tf.json`, JSON.stringify(mdaaTfOverride)); } } private prepLocalPackage(logPrefix: string, npmPackage: string, npmPackageNoVersion: string): string { const prefix = this.localPackages[npmPackage]; console.log(`Module ${logPrefix}: Package ${npmPackageNoVersion} found in local codebase. Running build.`); const buildCmd = `npx lerna run build --scope ${npmPackageNoVersion} --loglevel warn`; const fullBuildCmd = `cd '${__dirname}/../../../' && ${buildCmd} && cd '${this.cwd}'`; console.log(`Running Lerna Build: ${fullBuildCmd}`); this.execCmd(fullBuildCmd); return prefix; } private installPackage(logPrefix: string, npmPackage: string, npmPackageNoVersion: string): string { const prefix = path.resolve( `${this.workingDir}/nodejs/${MdaaDeploy.hashCodeHex(npmPackage, this.npmTag || 'latest').replace(/^-/, '')}`, ); console.log(`Module ${logPrefix}: Prepping NPM Package ${npmPackage}`); // nosemgrep /* istanbul ignore next */ if (!fs.existsSync(`${prefix}/package.json`)) { console.log(`Module ${logPrefix}: Installing ${npmPackage} to ${prefix}.`); //Install the module CDK App NPM package const npmInstallCmd = `npm install --no-fund --tag '${this.npmTag}' --prefix '${prefix}' '${npmPackage}' ${ this.npmDebug ? '-d' : ' > /dev/null' }`; // console.log( `Running NPM Install Cmd: ${ npmInstallCmd }` ) this.execCmd(`mkdir -p '${prefix}' && ${npmInstallCmd}`); } else { console.log(`Module ${logPrefix}: Install prefix ${prefix} already exists. Attempting update instead.`); if (!this.updateCache[prefix]) { const npmUpdateCmd = `npm update --no-fund --tag '${this.npmTag}' --prefix '${prefix}' ${ this.npmDebug ? '-d' : ' > /dev/null' }`; // console.log( `Running NPM Update Cmd: ${ npmUpdateCmd }` ) this.execCmd(npmUpdateCmd); this.updateCache[prefix] = true; } else { console.log(`Module ${logPrefix}: Skipping update. Already updated this prefix.`); } } return `${prefix}/node_modules/${npmPackageNoVersion}`; } private prepCdkModule(moduleEffectiveConfig: ModuleEffectiveConfig): ModuleDeploymentConfig { const effectivePackageVersion = moduleEffectiveConfig.effectiveMdaaVersion || this.npmTag; const initialCdkAppNpmPackage = effectivePackageVersion ? `${moduleEffectiveConfig.modulePath}@${effectivePackageVersion}` : moduleEffectiveConfig.modulePath; const finalModuleCdkAppNpmPackage = moduleEffectiveConfig.modulePath.replace(/^@/, '').includes('@') ? moduleEffectiveConfig.modulePath : initialCdkAppNpmPackage; const logPrefix = `${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${moduleEffectiveConfig.moduleName}`; const [modulePath, localModule] = this.prepNpmPackage( logPrefix, finalModuleCdkAppNpmPackage.replace(/caef/, 'mdaa'), ); const moduleInstalledConfig: ModuleEffectiveConfig = { ...moduleEffectiveConfig, modulePath: modulePath, }; return { ...moduleInstalledConfig, moduleCmds: [this.createCdkCommand(moduleInstalledConfig, localModule)], localModule: localModule, }; } private prepNpmPackage(logPrefix: string, npmPackageName: string): [string, boolean] { const npmPackageNoVersion = npmPackageName.replace(/(?<!^)@.*/, ''); return npmPackageName in this.localPackages ? [this.prepLocalPackage(logPrefix, npmPackageName, npmPackageNoVersion), true] : [this.installPackage(logPrefix, npmPackageName, npmPackageNoVersion), false]; } private computeModuleDeployStage(moduleDeployConfig: ModuleDeploymentConfig): string { const moduleMdaaDeployConfigFile = `${moduleDeployConfig.modulePath}/mdaa.config.json`; // nosemgrep if (fs.existsSync(moduleMdaaDeployConfigFile)) { // nosemgrep const moduleMdaaDeployConfig = require(moduleMdaaDeployConfigFile); if ('DEPLOY_STAGE' in moduleMdaaDeployConfig) { const deployStage = moduleMdaaDeployConfig['DEPLOY_STAGE']; console.log( `Module ${moduleDeployConfig.domainName}/${moduleDeployConfig.envName}/${moduleDeployConfig.moduleName}: Set deploy stage to ${deployStage} by mdaa.config.json`, ); return deployStage; } } console.log( `Module ${moduleDeployConfig.domainName}/${moduleDeployConfig.envName}/${moduleDeployConfig.moduleName}: Set deploy stage to ${MdaaDeploy.DEFAULT_DEPLOY_STAGE} by default`, ); return MdaaDeploy.DEFAULT_DEPLOY_STAGE; } private deployModule(moduleDeploymentConfig: ModuleDeploymentConfig) { if (!this.devopsMode) { console.log(`\n-----------------------------------------------------------`); console.log( `Module ${moduleDeploymentConfig.domainName}/${moduleDeploymentConfig.envName}/${moduleDeploymentConfig.moduleName}: Running ${this.action}`, ); console.log(`-----------------------------------------------------------`); } moduleDeploymentConfig.moduleCmds.forEach(moduleCmd => { console.log( `Module ${moduleDeploymentConfig.domainName}/${moduleDeploymentConfig.envName}/${moduleDeploymentConfig.moduleName}: Running cmd:\n${moduleCmd}`, ); this.execCmd(`cd '${moduleDeploymentConfig.modulePath}' && ${moduleCmd}`); }); } private createCdkCommand(moduleEffectiveConfig: ModuleEffectiveConfig, localModule: boolean): string { const action = this.action == 'deploy' ? `${this.action} --all` : this.action; const cdkEnv: string[] = this.createCdkCommandEnv(moduleEffectiveConfig); const cdkCmd: string[] = []; cdkCmd.push( `npx ${this.npmDebug ? '-d' : ''} cdk ${action} ${this.cdkVerbose ? '-v' : ''} --require-approval never`, ); if (!localModule) { cdkCmd.push(`-a 'npx ${this.npmDebug ? '-d' : ''} ${moduleEffectiveConfig.modulePath}/'`); } cdkCmd.push( `-o '${this.workingDir}/cdk.out/${this.config.contents.organization}/${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${moduleEffectiveConfig.moduleName}'`, ); cdkCmd.push(`-c 'org=${this.config.contents.organization}'`); cdkCmd.push(`-c 'env=${moduleEffectiveConfig.envName}'`); cdkCmd.push(`-c 'module_name=${moduleEffectiveConfig.moduleName}'`); cdkCmd.push(`-c 'domain=${moduleEffectiveConfig.domainName}'`); if (this.config.contents.naming_module && this.config.contents.naming_class) { cdkCmd.push(`-c 'naming_module=${moduleEffectiveConfig.customNaming?.naming_module}'`); cdkCmd.push(`-c 'naming_class=${moduleEffectiveConfig.customNaming?.naming_class}'`); } else if (this.config.contents.naming_module || this.config.contents.naming_class) { throw new Error("Both 'naming_module' and 'naming_class' must be specified together."); } this.addOptionalCdkContextStringParam(cdkCmd, 'use_bootstrap', moduleEffectiveConfig.useBootstrap?.toString()); this.addOptionalCdkContextStringParam( cdkCmd, 'module_configs', moduleEffectiveConfig.moduleConfigFiles?.map(x => path.resolve(x)).join(','), ); this.addOptionalCdkContextStringParam( cdkCmd, 'tag_configs', moduleEffectiveConfig.tagConfigFiles?.map(x => path.resolve(x)).join(','), ); this.addOptionalCdkContextStringParam( cdkCmd, 'additional_accounts', moduleEffectiveConfig.additionalAccounts?.join(','), ); this.addOptionalCdkContextStringParam( cdkCmd, 'log_suppressions', this.config.contents.log_suppressions?.toString(), ); this.addOptionalCdkContextObjParam(cdkCmd, 'custom_aspects', moduleEffectiveConfig.customAspects); this.addOptionalCdkContextObjParam(cdkCmd, 'module_config_data', moduleEffectiveConfig.effectiveModuleConfig); this.addOptionalCdkContextObjParam(cdkCmd, 'tag_config_data', moduleEffectiveConfig.effectiveTagConfig); if (this.roleArn) { cdkCmd.push(`-r '${this.roleArn}'`); } cdkCmd.push(...generateContextCdkParams(moduleEffectiveConfig)); if (this.cdkPushdown) { console.log( `Module ${moduleEffectiveConfig.domainName}/${moduleEffectiveConfig.envName}/${ moduleEffectiveConfig.moduleName }: CDK Pushdown Options: ${JSON.stringify(this.cdkPushdown, undefined, 2)}`, ); cdkCmd.push(...this.cdkPushdown); } return cdkEnv.length > 0 ? `${cdkEnv.join(' && ')} && ${cdkCmd.join(' \\\n\t')}` : cdkCmd.join(' \\\n\t'); } private addOptionalCdkContextStringParam(cdkCmd: string[], context_key: string, context_value?: string) { if (context_value) { cdkCmd.push(`-c '${context_key}=${context_value}'`); } } private addOptionalCdkContextObjParam( cdkCmd: string[], context_key: string, context_value?: MdaaCustomAspect[] | TagElement | ConfigurationElement, ) { if (context_value) { if (Object.keys(context_value).length > 0) { const context_string_value = JSON.stringify(JSON.stringify(context_value)); cdkCmd.push(`-c ${context_key}=${context_string_value}`); } } } private createCdkCommandEnv(moduleEffectiveConfig: ModuleEffectiveConfig): string[] { const cdkEnv: string[] = []; /* istanbul ignore next */ if (this.config.contents.region && this.config.contents.region.toLowerCase() != 'default') { cdkEnv.push(`export CDK_DEPLOY_REGION=${this.config.contents.region}`); cdkEnv.push(`export AWS_DEFAULT_REGION=${this.config.contents.region}`); } /* istanbul ignore next */ if (moduleEffectiveConfig.deployAccount && moduleEffectiveConfig.deployAccount.toLowerCase() != 'default') { cdkEnv.push(`export CDK_DEPLOY_ACCOUNT=${moduleEffectiveConfig.deployAccount}`); } return cdkEnv; } private computeDomainEffectiveConfig( domainName: string, domain: MdaaDomainConfig, globalEffectiveConfig: EffectiveConfig, ): DomainEffectiveConfig { return { ...globalEffectiveConfig, domainName: domainName, envTemplates: { ...globalEffectiveConfig.envTemplates, ...domain.env_templates }, effectiveContext: this.computeEffectiveContext(globalEffectiveConfig, domain.context), effectiveTagConfig: this.computeEffectiveTagConfig(globalEffectiveConfig, domain.tag_config_data), tagConfigFiles: this.computeEffectiveTagConfigFiles(globalEffectiveConfig, domain.tag_configs), effectiveMdaaVersion: this.computeEffectiveMdaaVersion(globalEffectiveConfig, this.config.contents.mdaa_version), customAspects: this.computeEffectiveCustomAspects(globalEffectiveConfig, domain.custom_aspects), customNaming: this.computeEffectiveCustomNaming(globalEffectiveConfig, domain.custom_naming), terraform: this.computeEffectiveTerraformConfig(globalEffectiveConfig, domain.terraform), }; } private computeEnvEffectiveConfig( envName: string, env: MdaaEnvironmentConfig, domainEffectiveConfig: DomainEffectiveConfig, ): EnvEffectiveConfig { return { ...domainEffectiveConfig, envName: envName, deployAccount: env.account, useBootstrap: env.use_bootstrap == undefined || env.use_bootstrap, effectiveContext: this.computeEffectiveContext(domainEffectiveConfig, env.context), effectiveTagConfig: this.computeEffectiveTagConfig(domainEffectiveConfig, env.tag_config_data), tagConfigFiles: this.computeEffectiveTagConfigFiles(domainEffectiveConfig, env.tag_configs), effectiveMdaaVersion: this.computeEffectiveMdaaVersion(domainEffectiveConfig, env.mdaa_version), customAspects: this.computeEffectiveCustomAspects(domainEffectiveConfig, env.custom_aspects), customNaming: this.computeEffectiveCustomNaming(domainEffectiveConfig, env.custom_naming), terraform: this.computeEffectiveTerraformConfig(domainEffectiveConfig, env.terraform), }; } private computeModuleEffectiveConfig( mdaaModuleName: string, mdaaModule: MdaaModuleConfig, envEffectiveConfig: EnvEffectiveConfig, ): ModuleEffectiveConfig { const modulePath = mdaaModule.module_path ? mdaaModule.module_path : mdaaModule.cdk_app; //NOSONAR if (!modulePath) { throw new Error('One of cdp_app or module_path must be defined'); } return { ...envEffectiveConfig, moduleName: mdaaModuleName, useBootstrap: envEffectiveConfig.useBootstrap && (mdaaModule.use_bootstrap == undefined || mdaaModule.use_bootstrap), moduleConfigFiles: [...(mdaaModule.app_configs || []), ...(mdaaModule.module_configs || [])], //NOSONAR effectiveModuleConfig: { ...(mdaaModule.app_config_data || {}), ...(mdaaModule.module_config_data || {}) }, //NOSONAR moduleType: mdaaModule.module_type ?? 'cdk', modulePath: modulePath, additionalAccounts: mdaaModule.additional_accounts, mdaaCompliant: mdaaModule.mdaa_compliant, effectiveContext: this.computeEffectiveContext(envEffectiveConfig, mdaaModule.context), effectiveTagConfig: this.computeEffectiveTagConfig(envEffectiveConfig, mdaaModule.tag_config_data), effectiveMdaaVersion: this.computeEffectiveMdaaVersion(envEffectiveConfig, mdaaModule.mdaa_version), tagConfigFiles: this.computeEffectiveTagConfigFiles(envEffectiveConfig, mdaaModule.tag_configs), customAspects: this.computeEffectiveCustomAspects(envEffectiveConfig, mdaaModule.custom_aspects), customNaming: this.computeEffectiveCustomNaming(envEffectiveConfig, mdaaModule.custom_naming), terraform: this.computeEffectiveTerraformConfig(envEffectiveConfig, mdaaModule.terraform), }; } private computeEffectiveTerraformConfig( parent: EffectiveConfig, child?: TerraformConfig, ): TerraformConfig | undefined { const _ = require('lodash'); return _.mergeWith(child, parent.terraform); } private computeEffectiveCustomNaming( parent: EffectiveConfig, child?: MdaaCustomNaming, ): MdaaCustomNaming | undefined { return child || parent.customNaming; } private computeEffectiveCustomAspects(parent: EffectiveConfig, child?: MdaaCustomAspect[]): MdaaCustomAspect[] { return [...(parent.customAspects || []), ...(child || [])]; } private computeEffectiveTagConfigFiles(parent: EffectiveConfig, child?: string[]): string[] { return [...(parent.tagConfigFiles || []), ...(child || [])]; } private computeEffectiveMdaaVersion(parent: EffectiveConfig, child?: string): string | undefined { return child || parent.effectiveMdaaVersion; } private computeEffectiveTagConfig(parent: EffectiveConfig, child?: TagElement): TagElement { return { ...parent.effectiveTagConfig, ...child, }; } private computeEffectiveContext(parent: EffectiveConfig, child?: ConfigurationElement): ConfigurationElement { return { ...parent.effectiveContext, ...child, }; } /* istanbul ignore next */ private execCmd(cmd: string) { // nosemgrep if (!this.testMode) { try { require('child_process').execSync(cmd, { stdio: 'inherit' }); } catch (error: unknown) { if (this.noFail) { console.warn(`Child process raised exception: ${error}`); if (error instanceof Error) { console.error(error.stack); } } else { throw error; } } } else { console.log(`Testing Mode:\n ${cmd}`); } } protected static hashCodeHex(...strings: string[]) { let h = 0; strings.forEach(s => { for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; }); return h.toString(16); } }