|
#!/usr/bin/env bash |
|
|
|
# ################################################## |
|
# Purpose: Create README.md from ${HEADER}/${FOOTER} and build output-${GEOMODULE}.tf from included modules |
|
# |
|
version="1.0.4" |
|
# |
|
# HISTORY: |
|
# |
|
# * 19-Jun-2018 - v1.0.4 - Fixing logic bug determining if on the right tag |
|
# |
|
# ################################################## |
|
|
|
GEOMODULE=$(basename $(pwd)) |
|
GEOMODULE=${GEOMODULE//-/_} |
|
GOUT=output-${GEOMODULE}.tf |
|
|
|
HEADER=.header.local.md |
|
FOOTER=.footer.local.md |
|
|
|
function mainScript() { |
|
TFDOC=$(which terraform-docs_linux_amd64) |
|
if [ "${TFDOC}" == "" ]; then |
|
TFDOC=$(which terraform-docs) |
|
if [ "${TFDOC}" == "" ]; then |
|
error "You need a binary of terraform-docs in your path" |
|
die |
|
fi |
|
fi |
|
|
|
if [ ! -d .terraform ]; then |
|
die "You must init the local directory using ${bold}init-terraform${reset} first" |
|
fi |
|
|
|
RM1=$(mktemp) |
|
RM2=$(mktemp) |
|
|
|
rm -rf ${GOUT} |
|
touch ${GOUT} |
|
|
|
cat .terraform/modules/modules.json | jq -rc '.Modules[] | select(.Key | test("^[^|]*$"))' | while read JSON; do |
|
DIR="$(echo "${JSON}" | jq -r .Dir)" |
|
KEY="$(echo "${JSON}" | jq -r .Key | sed 's/^1\.//' | sed 's/;.*$//')" |
|
SOURCE="$(echo "${JSON}" | jq -r .Source)" |
|
# debug "JSON=${JSON}" |
|
if [[ "${SOURCE}" == *"cloudposse/terraform-terraform-label.git"* ]]; then |
|
debug "Ignoring module ${KEY} (${SOURCE})" |
|
continue |
|
fi |
|
if [[ "${SOURCE}" == *"cloudposse/terraform-null-label.git"* ]]; then |
|
debug "Ignoring module ${KEY} (${SOURCE})" |
|
continue |
|
fi |
|
if [ ! -d ${DIR} ]; then |
|
continue |
|
fi |
|
debug "Processing ${bold}${DIR}${reset}, terraform module ${bold}${KEY}${reset}" |
|
|
|
debug "Source=${SOURCE}" |
|
REPO=$(echo "${SOURCE}" | sed -r "s/^.*github.com\/((\/\w+)*\/)?([\w\-\.]*[^#? \t]+)?(\?[a-z]+=)?(.*)?(#[\w\-]+)?$/\3/") |
|
TAG=$(echo "${SOURCE}" | sed -r "s/^.*github.com\/((\/\w+)*\/)?([\w\-\.]*[^#? \t]+)?(\?[a-z]+=)?(.*)?(#[\w\-]+)?$/\5/") |
|
if [ "${cache[${REPO}]}" != "" ]; then |
|
LATEST="${cache[${REPO}]}" |
|
else |
|
LATEST=$(curl --silent "https://api.github.com/repos/${REPO/.git/}/tags?${GITHUB_ACCESS_TOKEN}" | jq -r '.[0]?.name') |
|
cache[${REPO}]="${LATEST}" |
|
fi |
|
debug "REPO=${REPO} TAG=${TAG} and latest=${LATEST}" |
|
if [ "${LATEST}" == "" ]; then |
|
notice "Repo ${REPO} does not appear to use tags. Please suggest they tag their revisions for repeatability" |
|
elif [ "${TAG}" == "master" ]; then |
|
warning "You should NOT be referencing master. We recommend using ${REPO}?refs=tags/${LATEST} instead" |
|
elif [ "${TAG}" == "${LATEST}" ]; then |
|
debug "You are using what appears to be the latest version (${LATEST})" |
|
elif [ "${TAG}" == "tags/${LATEST}" ]; then |
|
debug "You are using what appears to be the latest version (${LATEST})" |
|
else |
|
notice "NOTE: The latest tag for ${REPO} is ${LATEST} (you are using ${TAG})" |
|
fi |
|
|
|
echo "### Module \`${KEY}\` Inputs and Outputs: " >> ${RM1} |
|
|
|
if ${readme}; then |
|
debug "Using ${TFDOC} to get the documentation for ${bold}${KEY}${reset} and adding it to README.md" |
|
${TFDOC} md ${DIR} >> ${RM1} |
|
fi |
|
|
|
break >> ${RM1} |
|
|
|
if ${output}; then |
|
debug "Now using ${TFDOC} to get the outputs of for module ${bold}${KEY}${reset} and create our ${bold}${GOUT}${reset} file" |
|
${TFDOC} json ${DIR} | jq -rc ".Outputs[]? " | while read OUTPUT; do |
|
NAME=$(echo "${OUTPUT}" | jq -r .Name) |
|
DESC=$(echo "${OUTPUT}" | jq -r .Description?) |
|
NEW_KEY=$(echo "${GEOMODULE}_${KEY}_${NAME}" | sed "s/${GEOMODULE}_\(${GEOMODULE}_\)*/${GEOMODULE}_/g") |
|
info "Variable ${bold}${NAME}${reset} from ${bold}${KEY}${reset} be output as ${bold}${NEW_KEY}${reset}" |
|
cat <<EOF >> ${GOUT} |
|
output "${NEW_KEY}" { |
|
value = "\${module.${KEY}.${NAME}}" |
|
description = "${DESC}" |
|
} |
|
|
|
EOF |
|
done |
|
fi |
|
done |
|
|
|
if ${readme}; then |
|
if [ -f README.md -a ! -f ${HEADER} ]; then |
|
seek_confirmation "Warning -- this overwrites ${bold}README.md${reset} -- break now or suffer the consequences (I'm warning you because you don't have a ${HEADER} file)" |
|
if ! is_confirmed; then |
|
die |
|
fi |
|
fi |
|
|
|
rm README.md -f |
|
touch README.md |
|
|
|
if [ -f ${HEADER} ]; then |
|
debug "You have a ${HEADER}, adding that to ${bold}README.md${reset}" |
|
cat ${HEADER} > README.md |
|
fi |
|
|
|
echo "# Module \`${GEOMODULE}\` Inputs and Outputs" >> README.md |
|
|
|
debug "Running terraform-docs to build your documentation for this directory" |
|
${TFDOC} md . >> README.md |
|
|
|
break >> README.md |
|
cat ${RM1} >> README.md |
|
|
|
if [ -f ${FOOTER} ]; then |
|
cat ${FOOTER} >> README.md |
|
fi |
|
notice "${bold}README.md${reset} created" |
|
fi |
|
if ${output}; then |
|
notice "${bold}${GOUT}${reset} created" |
|
fi |
|
echo " " |
|
} |
|
|
|
function break { |
|
echo " " |
|
echo "---" |
|
echo " " |
|
} |
|
|
|
# Options and Usage |
|
# ----------------------------------- |
|
function usage() { |
|
echo -n "${scriptName} [OPTION]... |
|
|
|
Fundamentally this script can do 2 things: |
|
|
|
- It creates ${bold}output-modules.tf${reset} which outputs all the input modules into the state |
|
- It creates ${bold}README.md${reset} with your input/outputs for this module (for reference), but also gives you the module input/outputs you are using in this directory as reference |
|
|
|
${bold}Options:${reset} |
|
--no-readme Don't build the ${bold}README.md${reset} file |
|
--no-output-modules Don't build the ${bold}output-modules.tf${reset} file |
|
-p, --prefix Define the prefix to use instead of '${bold}${GEOMODULE}${reset}' -- used in the ${bold}output-modules.tf${reset} file as the module prefix |
|
-o, --output-name Define the name for the output modules (currently ${bold}output-modules.tf${reset}) |
|
-l, --log-level Set the display logging level (default=${bold}notice${reset}. Valid values are debug|info|notice |
|
-d, --debug Set logging level to debug (shortcut) |
|
-n, --notice Set logging level to notice (shortcut) |
|
-h, --help Display this help and exit |
|
--version Output version information and exit |
|
|
|
NOTES: |
|
|
|
* If you have ${HEADER} and/or ${FOOTER} in this directory, those will be prepended/appended to the README.md |
|
* If ${bold}WILL${reset} a github API rate limit trying to get the latest releases (null comes back for versions). Create your own authentication by setting: |
|
|
|
export GITHUB_ACCESS_TOKEN=access_token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
|
|
|
" |
|
} |
|
|
|
function process_user_options() { |
|
# Print help if no arguments were passed. |
|
# Uncomment to force arguments when invoking the script |
|
# ------------------------------------- |
|
#[[ $# -eq 0 ]] && set -- "--help" |
|
|
|
# Set Flags |
|
quiet=true |
|
printLog=false |
|
logLevel=info |
|
force=false |
|
strict=false |
|
debug=false |
|
readme=true |
|
output=true |
|
args=() |
|
|
|
# Read the options and set stuff |
|
while [[ ${1} = -?* ]]; do |
|
case ${1} in |
|
--no-readme) readme=false; notice "Not building ${bold}README.md${reset}" ;; |
|
--no-output-modules) output=false notice "Not building ${bold}${GOUT}${reset}" ;; |
|
-h|--help) usage >&2; safeExit ;; |
|
--version) echo "$(basename ${0}) ${version}"; safeExit ;; |
|
-p|--prefix) shift; GEOMODULE=${1}; GOUT=output-${GEOMODULE}.tf; notice "Changing the prefix to ${bold}${GEOMODULE}${reset}" ;; |
|
-o|--output-name) shift; GOUT=${1}; notice "Changing the output modules name to ${bold}${GOUT}${reset}" ;; |
|
-l|--log-level) shift; logLevel=${1}; shift; ;; |
|
-d|--debug) shift; logLevel=debug; shift; ;; |
|
-n|--notice) shift; logLevel=notice; shift; ;; |
|
--endopts) shift; break ;; |
|
*) usage; die "invalid option: '${1}'." ;; |
|
esac |
|
shift |
|
done |
|
|
|
# Store the remaining part as arguments. |
|
args+=("$@") |
|
} |
|
|
|
### |
|
### ----------------------[ No editing normally below here ]---------------------- |
|
### |
|
|
|
# Define log levels |
|
# ---------------------- |
|
declare -A logLevels=(["debug"]=0 ["info"]=1 ["notice"]=2) |
|
declare -A cache=() |
|
|
|
# Set Base Variables |
|
# ---------------------- |
|
scriptName=$(basename "${0}") |
|
|
|
# Logging |
|
# ----------------------------------- |
|
# Log is only used when the '-l' flag is set. |
|
logFile="/tmp/${scriptBasename}.log" |
|
|
|
function trapCleanup() { |
|
echo "" |
|
# Delete temp files, if any |
|
if [ -d "${tmpDir}" ] ; then |
|
rm -r "${tmpDir}" |
|
fi |
|
die "Exit trapped. In function: '${FUNCNAME[*]}'" |
|
} |
|
|
|
function safeExit() { |
|
# Delete temp files, if any |
|
if [ -d "${tmpDir}" ] ; then |
|
rm -r "${tmpDir}" |
|
fi |
|
trap - INT TERM EXIT |
|
exit |
|
} |
|
|
|
# Set Colors |
|
bold=$(tput bold) |
|
reset=$'\e[0m' |
|
purple=$'\e[1;35m' |
|
red=$'\e[1;31m' |
|
green=$'\e[1;32m' |
|
tan=$'\e[1;36m' |
|
blue=$'\e[1;34m' |
|
underline=$(tput sgr 0 1) |
|
|
|
# Set Temp Directory |
|
tmpDir="/tmp/${scriptName}.${RANDOM}.${RANDOM}.${RANDOM}.$" |
|
(umask 077 && mkdir "${tmpDir}") || { |
|
die "Could not create temporary directory! Exiting." |
|
} |
|
|
|
# Logging & Feedback |
|
# ----------------------------------------------------- |
|
function _alert() { |
|
case ${1} in |
|
debug|notice|info) |
|
[[ ${logLevels[${1}]} ]] || return 1 |
|
|
|
#check if level is enough |
|
(( ${logLevels[${1}]} < ${logLevels[$logLevel]} )) && return 2 |
|
;; |
|
esac |
|
case ${1} in |
|
error) local color="${bold}${red}"; ;; |
|
warning) local color="${red}"; ;; |
|
success) local color="${green}"; ;; |
|
debug) local color="${purple}"; ;; |
|
header) local color="${bold}${tan}"; ;; |
|
input) local color="${bold}"; ;; |
|
notice) local color="${green}"; ;; |
|
info) local color=""; ;; |
|
esac |
|
|
|
# Don't use colors on pipes or non-recognized terminals |
|
if [[ "${TERM}" != "xterm"* ]] || [ -t 1 ]; then color=""; reset=""; fi |
|
|
|
echo " " |
|
echo -e "$(date +"%r") ${color}$(printf "[%7s]" "${1}") ${_message}${reset}\\n"; |
|
|
|
# Print to Logfile |
|
if ${printLog} && [ "${1}" != "input" ]; then |
|
color=""; reset="" # Don't use colors in logs |
|
echo -e "$(date +"%m-%d-%Y %r") $(printf "[%7s]" "${1}") ${_message}" >> "${logFile}"; |
|
fi |
|
} |
|
|
|
function die () { local _message="${*} Exiting."; quiet=false; echo -e "$(_alert error)"; safeExit;} |
|
function error () { local _message="${*}"; echo -en "$(_alert error)"; } |
|
function warning () { local _message="${*}"; echo -en "$(_alert warning)"; } |
|
function notice () { local _message="${*}"; echo -en "$(_alert notice)"; } |
|
function info () { local _message="${*}"; echo -en "$(_alert info)"; } |
|
function debug () { local _message="${*}"; echo -en "$(_alert debug)"; } |
|
function success () { local _message="${*}"; echo -en "$(_alert success)"; } |
|
function input() { local _message="${*}"; echo -n "$(_alert input)"; } |
|
function header() { local _message="== ${*} == "; echo -e "$(_alert header)"; } |
|
function verbose() { if ${verbose}; then debug "$@"; fi } |
|
|
|
# SEEKING CONFIRMATION |
|
# ------------------------------------------------------ |
|
function seek_confirmation() { |
|
# echo "" |
|
input "$@" |
|
if "${force}"; then |
|
notice "Forcing confirmation with '--force' flag set" |
|
else |
|
read -p " (y/n) " -n 1 |
|
echo "" |
|
fi |
|
} |
|
function is_confirmed() { |
|
if "${force}"; then |
|
return 0 |
|
else |
|
if [[ "${REPLY}" =~ ^[Yy]$ ]]; then |
|
return 0 |
|
fi |
|
return 1 |
|
fi |
|
} |
|
function is_not_confirmed() { |
|
if "${force}"; then |
|
return 1 |
|
else |
|
if [[ "${REPLY}" =~ ^[Nn]$ ]]; then |
|
return 0 |
|
fi |
|
return 1 |
|
fi |
|
} |
|
|
|
|
|
# Iterate over options breaking -ab into -a -b when needed and --foo=bar into |
|
# --foo bar |
|
optstring=h |
|
unset options |
|
while (($#)); do |
|
case ${1} in |
|
# If option is of type -ab |
|
-[!-]?*) |
|
# Loop over each character starting with the second |
|
for ((i=1; i < ${#1}; i++)); do |
|
c=${1:i:1} |
|
|
|
# Add current char to options |
|
options+=("-$c") |
|
|
|
# If option takes a required argument, and it's not the last char make |
|
# the rest of the string its argument |
|
if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then |
|
options+=("${1:i+1}") |
|
break |
|
fi |
|
done |
|
;; |
|
|
|
# If option is of type --foo=bar |
|
--?*=*) options+=("${1%%=*}" "${1#*=}") ;; |
|
# add --endopts for -- |
|
--) options+=(--endopts) ;; |
|
# Otherwise, nothing special |
|
*) options+=("${1}") ;; |
|
esac |
|
shift |
|
done |
|
set -- "${options[@]}" |
|
unset options |
|
|
|
process_user_options $@ |
|
|
|
# Trap bad exits with your cleanup function |
|
trap trapCleanup EXIT INT TERM |
|
|
|
# Set IFS to preferred implementation |
|
IFS=$' \n\t' |
|
|
|
# Exit on error. Append '||true' when you run the script if you expect an error. |
|
set -o errexit |
|
|
|
# Run in debug mode, if set |
|
if ${debug}; then set -x ; fi |
|
|
|
# Exit on empty variable |
|
if ${strict}; then set -o nounset ; fi |
|
|
|
# Bash will remember & return the highest exitcode in a chain of pipes. |
|
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`, for example. |
|
set -o pipefail |
|
|
|
# Run your script |
|
mainScript |
|
|
|
# Exit cleanly |
|
safeExit |