-
-
Save tafkam/efba30a70d1ca4321ab42011b5b93d86 to your computer and use it in GitHub Desktop.
/** | |
* Wrapper around terragrunt to display output succinctly on Atlantis. | |
* | |
* Terragrunt is notoriously verbose, which can cause Atlantis to output | |
* hundreds of comments on single PRs, which can be annoying. | |
* | |
* This script will output just the final plan for resources to update on | |
* successful terragrunt runs, but will output all terragrunt output on | |
* errors. | |
*/ | |
const shell = require('shelljs'); | |
const path = require('path'); | |
const { PLANFILE } = process.env; | |
const logger = console; | |
/** | |
* A map of terraform field names to mask the output of to a Github | |
* Issue explaining why that field is masked | |
*/ | |
const maskMap = { | |
// Sensitive values from aws_cognito_identity_provider | |
client_id: | |
'https://github.com/terraform-providers/terraform-provider-aws/issues/9934', | |
client_secret: | |
'https://github.com/terraform-providers/terraform-provider-aws/issues/9934', | |
}; | |
/** | |
* A map of patterns for terragrunt modules that we would expect to fail to a Github | |
* Issue explaining why those modules are expected to fail. | |
* | |
* When these modules fail, we will display a message clearly stating that | |
* this is an expected behavior | |
*/ | |
const expectedFailingModules = { | |
'some/terragrunt/module': | |
'https://github.com/some-org/some-repo/issues/1234', | |
}; | |
/** | |
* Masks any blocklisted field names in the terraform output. | |
* | |
* Ideally, PRs would be sent to mark these fields as sensitive in | |
* the terraform provider itself, but this works as a temporary measure | |
* while fields those PRs are in review | |
* | |
* @param output - The original plan output | |
* @returns the plan output with sensitive values removed | |
*/ | |
function maskSensitiveValues(output) { | |
return Object.keys(maskMap).reduce( | |
(out, fieldName) => | |
out.replace( | |
new RegExp(`("${fieldName}" *=) ".*"`, 'g'), | |
(_, match) => | |
`${match} This field is sensitive and cannot be shown in PRs`, | |
), | |
output, | |
); | |
} | |
/** | |
* Promisifies shelljs.exec | |
* | |
* @param {string} command - Command to execute in the local shell | |
* @returns The resolved command | |
*/ | |
async function run(command) { | |
return new Promise((resolve) => { | |
shell.exec(command, { silent: true }, (code, stdout, stderr) => { | |
resolve({ code, stdout, stderr }); | |
}); | |
}); | |
} | |
/** | |
* Runs a plan via terragrunt. Output is only shown on error | |
*/ | |
async function runPlan() { | |
const wasExpectedToFail = Object.keys( | |
expectedFailingModules, | |
).some((pattern) => new RegExp(pattern).test(shell.pwd())); | |
if (wasExpectedToFail) { | |
logger.log( | |
`Atlantis does not currently support the module in ${shell.pwd()}. Please run this module locally`, | |
); | |
shell.touch(PLANFILE); | |
return; | |
} | |
const { code, stderr } = await run( | |
`terragrunt plan -no-color -out=${PLANFILE}`, | |
); | |
if (code !== 0) { | |
logger.log(stderr); | |
throw Error(`Failed to run plan in ${shell.pwd()}`); | |
} | |
} | |
/** | |
* Prints a representation of the terraform plan output to the console | |
*/ | |
async function printPlanFile() { | |
const { dir, base } = path.parse(PLANFILE); | |
shell.cd(dir); | |
const { stdout } = await run(`terragrunt show -no-color ${base}`); | |
logger.log(maskSensitiveValues(stdout)); | |
} | |
/** | |
* Runs an apply via terragrunt. Output is only shown on error | |
*/ | |
async function runAndPrintApply() { | |
const { code, stdout, stderr } = await run( | |
`terragrunt apply -no-color ${PLANFILE}`, | |
); | |
if (code !== 0) { | |
logger.log(stderr); | |
throw Error(`Failed to run apply in ${shell.pwd()}`); | |
} else { | |
logger.log(stdout); | |
} | |
} | |
/** | |
* Main function | |
*/ | |
async function main() { | |
const args = process.argv.slice(2); | |
const command = args[0]; | |
if (command.toString().trim() === 'apply') { | |
await runAndPrintApply(); | |
} else { | |
await runPlan(); | |
await printPlanFile(); | |
} | |
} | |
/** | |
* Run the program, exiting with a status code of 1 on any error | |
*/ | |
main().catch((err) => { | |
logger.error(err); | |
process.exit(1); | |
}); |
/** | |
* Wrapper around terragrunt to display output succinctly on Atlantis. | |
* | |
* Terragrunt is notoriously verbose, which can cause Atlantis to output | |
* hundreds of comments on single PRs, which can be annoying. | |
* | |
* This script will output just the final plan for resources to update on | |
* successful terragrunt runs, but will output all terragrunt output on | |
* errors. | |
*/ | |
const shell = require('shelljs'); | |
const path = require('path'); | |
const { TGPLAN } = process.env; | |
const logger = console; | |
/** | |
* A map of terraform field names to mask the output of to a Github | |
* Issue explaining why that field is masked | |
*/ | |
const maskMap = { | |
// Sensitive values from aws_cognito_identity_provider | |
client_id: | |
'https://github.com/terraform-providers/terraform-provider-aws/issues/9934', | |
client_secret: | |
'https://github.com/terraform-providers/terraform-provider-aws/issues/9934', | |
}; | |
/** | |
* A map of patterns for terragrunt modules that we would expect to fail to a Github | |
* Issue explaining why those modules are expected to fail. | |
* | |
* When these modules fail, we will display a message clearly stating that | |
* this is an expected behavior | |
*/ | |
const expectedFailingModules = { | |
'some/terragrunt/module': | |
'https://github.com/some-org/some-repo/issues/1234', | |
}; | |
/** | |
* Masks any blocklisted field names in the terraform output. | |
* | |
* Ideally, PRs would be sent to mark these fields as sensitive in | |
* the terraform provider itself, but this works as a temporary measure | |
* while fields those PRs are in review | |
* | |
* @param output - The original plan output | |
* @returns the plan output with sensitive values removed | |
*/ | |
function maskSensitiveValues(output) { | |
return Object.keys(maskMap).reduce( | |
(out, fieldName) => | |
out.replace( | |
new RegExp(`("${fieldName}" *=) ".*"`, 'g'), | |
(_, match) => | |
`${match} This field is sensitive and cannot be shown in PRs`, | |
), | |
output, | |
); | |
} | |
/** | |
* Promisifies shelljs.exec | |
* | |
* @param {string} command - Command to execute in the local shell | |
* @returns The resolved command | |
*/ | |
async function run(command) { | |
return new Promise((resolve) => { | |
shell.exec(command, { silent: true }, (code, stdout, stderr) => { | |
resolve({ code, stdout, stderr }); | |
}); | |
}); | |
} | |
/** | |
* Runs a plan via terragrunt. Output is only shown on error | |
*/ | |
async function runPlan() { | |
const wasExpectedToFail = Object.keys( | |
expectedFailingModules, | |
).some((pattern) => new RegExp(pattern).test(shell.pwd())); | |
if (wasExpectedToFail) { | |
logger.log( | |
`Atlantis does not currently support the module in ${shell.pwd()}. Please run this module locally`, | |
); | |
return; | |
} | |
const { code, stderr } = await run( | |
`terragrunt run-all plan -no-color -out="${TGPLAN}" --terragrunt-source-update --terragrunt-non-interactive --terragrunt-ignore-external-dependencies`, | |
); | |
if (code !== 0) { | |
logger.log(stderr); | |
throw Error(`Failed to run plan in ${shell.pwd()}`); | |
} | |
} | |
/** | |
* Prints a representation of the terraform plan output to the console | |
*/ | |
async function printPlanFile() { | |
const { stdout } = await run(`terragrunt run-all show "${TGPLAN}" -no-color --terragrunt-non-interactive --terragrunt-ignore-external-dependencies`); | |
logger.log(maskSensitiveValues(stdout)); | |
} | |
/** | |
* Runs an apply via terragrunt. Output is only shown on error | |
*/ | |
async function runAndPrintApply() { | |
const { code, stdout, stderr } = await run( | |
`terragrunt run-all apply -no-color --terragrunt-non-interactive --terragrunt-ignore-external-dependencies `, | |
); | |
if (code !== 0) { | |
logger.log(stderr); | |
throw Error(`Failed to run apply in ${shell.pwd()}`); | |
} else { | |
logger.log(stdout); | |
} | |
} | |
/** | |
* Main function | |
*/ | |
async function main() { | |
const args = process.argv.slice(2); | |
const command = args[0]; | |
if (command.toString().trim() === 'apply') { | |
await runAndPrintApply(); | |
} else { | |
await runPlan(); | |
await printPlanFile(); | |
} | |
} | |
/** | |
* Run the program, exiting with a status code of 1 on any error | |
*/ | |
main().catch((err) => { | |
logger.error(err); | |
process.exit(1); | |
}); |
I love it!
Have you tested this on a repo of yours?
did a small test case with the new terragrunt-atlantis-config features, to test a parent project with two sub projects each containing a terragrunt child. the run-all workflow should also for work for atlantis projects on the terragrunt child module hierarchy.
edit: larger projects might get cluttered with "no resources changed" log output, or terraform changes without reference to the actual terragrunt child.. definitely a caveat of the new hcl based projects. before you would know where the (non-)changes occur because atlantis wrote a separate comment for each tg child. not anymore! :)
actually did a catastrophic mistake here using the planfile for apply. this will apply terragrunt mock values for dependencies, which are not deployed yet at plan-time. run-all apply should be fine without a planfile
@dmattia terragrunt_runall_light.js basically just changes the PLANFILE env var prefilled by Atlantis to something arbitrary, so terragrunt will save it's planfiles in .terragrunt_cache and not the single planfile Atlantis defines for it's active pull-request.
It's important to not change the placement of the .terragrunt_cache directory, so the planfiles will be written in Atlantis' temporary pull-request directories.
Plan/Show/Apply were changed accordingly for a working terragrunt run-all workflow.