import commander from "commander" ; import fs from "fs" ; import fsextra from "fs-extra" ; import yaml from "js-yaml" ; import path from "path" ; import { Config } from "../../config" ; import { build as buildCmd , exit as exitCmd } from "../../lib/commandBuilder" ; import { logger } from "../../logger" ; import { ConfigYaml } from "../../types" ; import { SourceInformation , validateRemoteSource } from "./generate" ; import { BACKEND_TFVARS , DEFAULT_VAR_VALUE , DEFINITION_YAML , getSourceFolderNameFromURL , bedrockTemplatesPath , TERRAFORM_TFVARS , VARIABLES_TF , } from "./infra_common" ; import decorator from "./scaffold.decorator.json" ; import { build as buildError , log as logError } from "../../lib/errorBuilder" ; import { errorStatusCode } from "../../lib/errorStatusCode" ; export interface CommandOptions { name : string ; source : string ; template : string ; version : string ; } param config param opts export const validateValues = ( config , opts ) : void => { if ( ! config . azure_devops || ! config . azure_devops . access_token || ! config . azure_devops . infra_repository ) { logger . info ( `The infrastructure repository containing the remote terraform \
template repo and access token was not specified in bedrock-config.yml. Checking passed arguments.` ) ; if ( ! opts . source ) { throw buildError ( errorStatusCode . VALIDATION_ERR , "infra-scaffold-cmd-src-missing" ) ; } } if ( ! opts . name || ! opts . version || ! opts . template ) { throw buildError ( errorStatusCode . VALIDATION_ERR , "infra-scaffold-cmd-values-missing" ) ; } logger . info ( `All required options are configured via command line for \
scaffolding, expecting public remote repository for terraform templates \
or PAT embedded in source URL.` ) ; } ; export const constructSource = ( config ) : string => { const devops = config . azure_devops ! ; const source = `https://bedrock:${ devops . access_token }@${ devops . infra_repository }` ; logger . info ( `Infrastructure repository detected from initialized bedrock-config.yaml.` ) ; return source ; } ; param templatePath param envName export const copyTfTemplate = async ( templatePath , envName , generation ) : Promise < void > => { try { if ( generation === true ) { await fsextra . copy ( templatePath , envName , { filter : ( file ) => file . indexOf ( TERRAFORM_TFVARS ) === - 1 && file . indexOf ( BACKEND_TFVARS ) === - 1 , } ) ; } else { await fsextra . copy ( templatePath , envName , { filter : ( file ) => file . indexOf ( TERRAFORM_TFVARS ) === - 1 , } ) ; } logger . info ( `Terraform template files copied from ${ templatePath }` ) ; } catch ( err ) { throw buildError ( errorStatusCode . ENV_SETTING_ERR , { errorKey : "infra-err-locate-tf-env" , values : [ templatePath ] , } , err ) ; } } ; param templatePath export const validateVariablesTf = ( templatePath ) : void => { if ( ! fs . existsSync ( templatePath ) ) { throw buildError ( errorStatusCode . ENV_SETTING_ERR , { errorKey : "infra-err-tf-path-not-found" , values : [ VARIABLES_TF , templatePath ] , } ) ; } logger . info ( `Terraform ${ VARIABLES_TF } file found. Attempting to generate ${ DEFINITION_YAML } file.` ) ; } ; param dir export const validateBackendTfvars = ( dir ) : boolean => { const backendConfig = path . join ( dir , BACKEND_TFVARS ) ; if ( fs . existsSync ( backendConfig ) ) { logger . info ( `A remote backend configuration was found : ${ backendConfig }` ) ; return true ; } logger . info ( `No remote backend configuration was found.` ) ; return false ; } ; param { string } data export const parseVariablesTf = ( data ) : { [ key ] : string ; } => { const splitRegex = /^variable/gm ; const blocks = data . split ( splitRegex ) ; const fields = { } ; const fieldSplitRegex = /"\s{0,}\{/ ; const defaultRegex = /default\s{0,}=\s{0,}(.*)/ ; blocks . forEach ( ( b ) => { b = b . trim ( ) ; const elt = b . split ( fieldSplitRegex ) ; elt [ 0 ] = elt [ 0 ] . trim ( ) . replace ( '"' , "" ) ; if ( elt [ 0 ] . length > 0 ) { const match = elt [ 1 ] . trim ( ) . match ( defaultRegex ) ; if ( match ) { let value = match [ 1 ] ; if ( value . substr ( 0 , 1 ) === '"' && value . substr ( value . length - 1 , 1 ) === '"' ) { value = value . substr ( 1 , value . length - 2 ) ; } fields [ elt [ 0 ] ] = value ; } else { fields [ elt [ 0 ] ] = "" ; } } } ) ; return fields ; } ; param backendTfvarData export const parseBackendTfvars = ( backendData ) : { [ key ] : string ; } => { const backend = { } ; const block = backendData . replace ( /=/g , ":" ) . split ( "\n" ) ; block . forEach ( ( b ) => { const elt = b . split ( ":" ) ; if ( elt [ 0 ] . length > 0 ) { backend [ elt [ 0 ] ] = elt [ 1 ] . replace ( /"/g , "" ) . replace ( /(?:\\[rn]|[\r\n]+)+/g , "" ) ; } } ) ; return backend ; } ; param values param backendTfvars param vartfData export const generateClusterDefinition = ( values , backendData , vartfData ) : { [ key ] : string | { [ key ] : string ; } ; } => { const fields = parseVariablesTf ( vartfData ) ; const defaultVariables = [ ] ; const def = { name : values . name , source : values . source , template : values . template , version : values . version , } ; if ( backendData !== "" ) { const backend = parseBackendTfvars ( backendData ) ; def . backend = backend ; } if ( Object . keys ( fields ) . length > 0 ) { const fieldDict = { } ; Object . keys ( fields ) . forEach ( ( key ) => { fieldDict [ key ] = fields [ key ] || DEFAULT_VAR_VALUE ; } ) ; Object . keys ( fieldDict ) . forEach ( ( key ) => { if ( fieldDict [ key ] !== DEFAULT_VAR_VALUE ) { delete fieldDict [ key ] ; defaultVariables . push ( key ) ; } } ) ; def . variables = fieldDict ; if ( defaultVariables . length > 0 ) { logger . info ( `Default values will be used for these variables:\n${ defaultVariables . join ( "\n" ) }. \nCustom values can be set for these variables in the definition.yaml file.` ) ; } } return def ; } ; param values export const scaffold = ( values ) : void => { try { const tfVariableFile = path . join ( values . name , VARIABLES_TF ) ; const backendTfvarsFile = path . join ( values . name , BACKEND_TFVARS ) ; const backendData = validateBackendTfvars ( values . name ) ? fs . readFileSync ( backendTfvarsFile , "utf8" ) : "" ; if ( fs . existsSync ( tfVariableFile ) ) { logger . info ( `A ${ VARIABLES_TF } file found : ${ tfVariableFile }` ) ; const data = fs . readFileSync ( tfVariableFile , "utf8" ) ; if ( data ) { const baseDef = generateClusterDefinition ( values , backendData , data ) ; const definitionYaml = yaml . safeDump ( baseDef ) ; const confPath = path . format ( { base : DEFINITION_YAML , dir : values . name , root : "/ignored" , } ) ; fs . writeFileSync ( confPath , definitionYaml , "utf8" ) ; } else { throw buildError ( errorStatusCode . ENV_SETTING_ERR , { errorKey : "infra-unable-read-var-file" , values : [ tfVariableFile ] , } ) ; } } } catch ( err ) { throw buildError ( errorStatusCode . EXE_FLOW_ERR , "infra-err-create-scaffold" , err ) ; } } ; param envPath export const removeTemplateFiles = ( envPath ) : void => { try { const files = fs . readdirSync ( envPath ) ; files . filter ( ( f ) => f !== DEFINITION_YAML ) . forEach ( ( f ) => { fs . unlinkSync ( path . join ( envPath , f ) ) ; } ) ; } catch ( e ) { logger . warn ( `cannot read ${ envPath }` ) ; } } ; export const execute = async ( config , opts , exitFn ) : Promise < void > => { try { validateValues ( config , opts ) ; opts . source = opts . source || constructSource ( config ) ; const scaffoldDefinition = { source : opts . source , template : opts . template , version : opts . version , } ; const sourceFolder = getSourceFolderNameFromURL ( opts . source ) ; const sourcePath = path . join ( bedrockTemplatesPath , sourceFolder ) ; await validateRemoteSource ( scaffoldDefinition ) ; await copyTfTemplate ( path . join ( sourcePath , opts . template ) , opts . name , false ) ; validateVariablesTf ( path . join ( sourcePath , opts . template , VARIABLES_TF ) ) ; scaffold ( opts ) ; removeTemplateFiles ( opts . name ) ; await exitFn ( 0 ) ; } catch ( err ) { logError ( buildError ( errorStatusCode . CMD_EXE_ERR , "infra-scaffold-cmd-failed" , err ) ) ; await exitFn ( 1 ) ; } } ; param command export const commandDecorator = ( command ) : void => { buildCmd ( command , decorator ) . action ( async ( opts ) => { const config = Config ( ) ; await execute ( config , opts , async ( status ) => { await exitCmd ( logger , process . exit , status ) ; } ) ; } ) ; } ;