-
-
Save 3coma3/0238e83a8e5d8b452071b1f2fbd29508 to your computer and use it in GitHub Desktop.
extensible Terraform wrapper "to type less", for category/module structures, with regex/direct match|queued/chained operations|macros|tfvar expansion|+
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
shopt -s extglob | |
# Globals ====================================================================== | |
export script_path="$(readlink -f "$0")" | |
export script_name="${script_path##*/}" | |
export mods_root="$(readlink -f "${0%/*}/../provision")" category='' module='' | |
export -a all_mods=() | |
export -A global_sws=() cat_sws=() mod_sws=() switches=() | |
export -A global_cmds=() cat_cmds=() mod_cmds=() | |
export -A tf_cmds=() tfvar_aliases=() | |
# Utilities ==================================================================== | |
# outputs associative array keys separated by a delimiter | |
get_keys() { | |
local array="$1" delim="${2:-|}" list | |
local -a 'ks=("${!'"$array"'[@]}")' | |
printf -v list "%s${delim}" "${ks[@]}" | |
echo ${list%?} | |
} | |
# tests if the key exists in the associative array | |
# | |
# RETURNS | |
# 0 if key exists, 1 if key doesn't exist | |
has_key() { | |
local array="$1" key="$2" | |
[[ "$(get_element $@)" == "1" ]] | |
} | |
# shortcut to test for a flag set from command line switch | |
is_on() { | |
has_key switches $1 | |
} | |
# prints the element if the key exists in the associative array | |
# | |
# RETURNS | |
# 0 if key exists, 1 if key doesn't exist | |
get_element() { | |
local array="$1" key="$2" | |
local -a 'clone=("${'"$array"'[@]}")' | |
[[ -v clone["$key"] ]] && { | |
echo "${clone[$key]}" | |
return 0 | |
} | |
} | |
# prints all the elements in the array that match the regex | |
# | |
# RETURNS | |
# 0 if matches, 1 if no matches | |
get_elements_regex() { | |
local array="$1" regex="$2" matches=0 | |
local -a 'clone=("${'"$array"'[@]}")' | |
for i in "${clone[@]}"; do | |
[[ "$i" =~ $regex ]] && { | |
echo "$i" | |
(( matches++ )) | |
} | |
done | |
(( matches != 0 )) | |
} | |
# lists all categories, all modules in a category, a combination of module and | |
# category, or categories/modules with the specified name | |
# | |
# PARAMS (can be passed in any order) | |
# cat=<"all" | category_name> | |
# mod=<"all" | module_name> | |
# | |
# RETURNS | |
# error code of the call to elements_regex | |
catmodfind() { | |
while [[ -n "$1" ]]; do | |
local "$1" ; shift | |
done | |
case "$cat" in | |
all) get_elements_regex all_mods . | catmodfilter cat | uniq | |
;; | |
'') get_elements_regex all_mods "/$mod\$" | |
;; | |
*) case "$mod" in | |
all) get_elements_regex all_mods "^$cat/" | catmodfilter mod | |
;; | |
'') get_elements_regex all_mods "^$cat/" | catmodfilter cat | uniq | |
;; | |
*) get_elements_regex all_mods "^$cat/$mod\$" | |
;; | |
esac | |
;; | |
esac | |
return ${PIPESTATUS[0]} | |
} | |
# filters the category or module name from a stream of category/module entries | |
# | |
# PARAMS | |
# $1 "cat" | "mod" | |
# $2 optional one-time entry | |
catmodfilter() { | |
local what="$1" onetime="$2" cat mod | |
[[ -n "$onetime" ]] && { | |
IFS=/ read cat mod <<< "$onetime" | |
echo "${!1}" | |
return | |
} | |
while IFS=/ read cat mod; do | |
echo "${!1}" | |
done | |
} | |
# expands the left hand side of a tfvar alias to its aliased value, formatted | |
# for use in a Terraform command line | |
# | |
# PARAMS | |
# $@ a tfvar alias string | |
tfvar_alias() { | |
local lhs="${@%%=*}" rhs="${@##*=}" | |
[[ -v tfvar_aliases[${lhs##@}] ]] && lhs="@${tfvar_aliases[${lhs##@}]}" | |
echo -var="${lhs##@}=${rhs}" | |
} | |
# prints path information relative to CWD before changing directory | |
cd_relative_to_cwd() { | |
local path="$@" | |
[[ "$path" == "$(pwd)" ]] && return | |
local relpath="$(sed -r "s|$(readlink -f $(pwd))/||" <<< "$path")" | |
echo | |
header "Changing CWD to '$relpath'" | |
cd "$path" | |
} | |
# User I/O and error handling ================================================== | |
# outputs its parameters (or stdin if there are no parameters) to standard error | |
# | |
# PARAMS | |
# $@ optional string | |
msg() { | |
(( $# )) && { | |
echo -e "$@" >&2 | |
return | |
} | |
local msg | |
while read msg; do | |
echo -e "$msg" | |
done >&2 | |
} | |
# prints the arguments as a header, filling the line with $char up to $col | |
header() { | |
local str="$@" char='\x2d' col=80 | |
local line="$(eval printf '${char}%.0s' {1..$(( col - ${#str} ))})" | |
msg "$@ $line\n" | |
} | |
# prompts a yes no question | |
# | |
# PARAMS | |
# $@ the prompt | |
# | |
# RETURNS | |
# 0 yes | |
# 1 no | |
yesno() { | |
local prompt="${1:-"Are you sure? (y/n)"}" result=1 | |
while :; do | |
read -p "$prompt " -n 1 -r result | |
echo | |
case "$result" in | |
y|Y) return 0 ;; | |
n|N) return 1 ;; | |
*) continue ;; | |
esac | |
done | |
} | |
# acts and exits according to an error code | |
# | |
# PARAMS | |
# $1 error code as defined in the case statement and/or the errcodes hash | |
# $@ any remaining arguments passed to the error handler | |
# | |
# RETURNS | |
# Matching errcodes[] value (defaults to EUNKNOWN) | |
error() { | |
local code="$1" | |
shift | |
local -A errcodes | |
errcodes[ENOARGS]=10 | |
errcodes[ECMDNOCAT]=11 | |
errcodes[ENOCAT]=12 | |
errcodes[ENOMOD]=13 | |
errcodes[ENOMODSELECT]=14 | |
errcodes[EBADFQID]=15 | |
errcodes[EMODEXISTS]=20 | |
errcodes[EUNKNOWN]=255 | |
case $code in | |
ECMDNOCAT) | |
msg <<EOF | |
A category command ($@) was specified, but no category is known at this point | |
Try to pass the command *after* a category locator. | |
Available module categories: | |
$(catmodfind cat=all) | |
EOF | |
;; | |
ENOCAT) | |
msg <<EOF | |
$([[ -z "$category" ]] || msg "Category not found: '$category'\n")Available module categories: | |
$(catmodfind cat=all) | |
EOF | |
;; | |
ENOMOD) | |
msg <<EOF | |
$([[ -z "$module" ]] || msg "Module not found: '$category/$module'\n")Available modules in the '$category' category: | |
$(catmodfind cat="$category" mod=all) | |
EOF | |
;; | |
ENOMODSELECT) | |
msg "\nAborting." | |
;; | |
EMODEXISTS) | |
msg "Can't create '$@': already exists." | |
;; | |
EBADFQID) | |
msg <<EOF | |
Invalid flag queue index: $1 | |
The problem seems to be around "... ${2} ${3# } \\\e[4m${4}\\\e[0m" | |
EOF | |
;; | |
EUNKNOWN) ;& | |
*) msg "\nUnknown error: ${code:-"no code was given"}, around line ${BASH_LINENO[0]}\n" | |
code=EUNKNOWN | |
;; | |
esac | |
[[ -v errcodes[$code] ]] || code=EUNKNOWN | |
exit ${errcodes[$code]} | |
} | |
# Switch handlers ============================================================== | |
sw_simple() { | |
local scope="$1" item="${2#--}" state="$3" | |
[[ "$state" == "off" ]] | |
switches[$scope_$item]=$? | |
} | |
# Command handlers ============================================================= | |
cmd_help() { | |
header "$script_name v2" | |
msg <<EOF | |
This help is a stub | |
SYNTAX | |
category_locator [category_commands | category_switches | global_switches] | |
module_locator [module_commands | module_switches | global_switches] | |
category_locator | |
category | |
tmod category | |
module_locator | |
category module | |
tmod category module | |
tmod module | |
tmod regex | |
EOF | |
} | |
cmd_add_module() { | |
shift | |
local newmod="$1" | |
[[ -d "$newmod" ]] && error EMODEXISTS "$category/$newmod" | |
header "Add '$category/$newmod'" | |
is_on gl_dry && return | |
mkdir -v "$newmod" | |
} | |
cmd_forward_shell() { | |
local cmdline="$@" | |
header "Forward to shell: '$cmdline'" | |
is_on gl_dry && return | |
$cmdline | |
} | |
cmd_forward_terraform() { | |
local cmdline | |
if (( ! $# )); then | |
msg <<EOF | |
You can add one of the below Terraform subcommands directly to this command line | |
The entire command line will be forwarded to Terraform. | |
-------------------------------------------------------------------------------- | |
EOF | |
cmdline="-help" | |
else cmdline="$@" | |
fi | |
header "Forward to Terraform: '$cmdline'" | |
is_on gl_dry && return | |
terraform $@ | |
} | |
cmd_mod_clean() { | |
header "Clean '$category/$module' state" | |
is_on gl_dry && return | |
find -iname "*tfstate*" -exec rm -v {} \; | |
(( $# )) && main $@ | |
} | |
cmd_mod_depclean() { | |
header "Clean '$category/$module' dependency cache" | |
is_on gl_dry && return | |
find -iname .terraform -exec rm -vrf {} \; | |
(( $# )) && main $@ | |
} | |
cmd_mod_reset() { | |
header "Reset '$category/$module'" | |
is_on gl_dry && return | |
cmd_mod_depclean clean init | |
(( $# )) && main $@ | |
} | |
cmd_mod_cycle() { | |
header "Cycle '$category/$module'" | |
cmd_mod_depclean init | |
main destroy $@ apply @ | |
} | |
cmd_mod_remove() { | |
header "Remove '$category/$module'" | |
is_on gl_dry && return | |
msg <<EOF | |
WARNING | |
You are about to REMOVE the module '$module' from the category '$category' | |
This operation cannot be reverted. Please confirm: | |
EOF | |
yesno && { | |
cd .. | |
rm -rfv "$module" | |
} | |
} | |
# Main functions =============================================================== | |
# performs value initializations | |
# | |
# PARAMS | |
# $@ <list of initialization sections to run> | |
init() { | |
local sections=( "$@" ) | |
for section in ${sections[@]}; do | |
case $section in | |
commands) | |
global_cmds[help]=cmd_help | |
cat_cmds[help]=cmd_help | |
cat_cmds[add]=cmd_add_module | |
cat_cmds[ls]=cmd_forward_shell | |
cat_cmds[git]=cmd_forward_shell | |
cat_cmds[grep]=cmd_forward_shell | |
cat_cmds[find]=cmd_forward_shell | |
mod_cmds[clean]=cmd_mod_clean | |
mod_cmds[depclean]=cmd_mod_depclean | |
mod_cmds[reset]=cmd_mod_reset | |
mod_cmds[cycle]=cmd_mod_cycle | |
mod_cmds[remove]=cmd_mod_remove | |
declare -gA 'tf_cmds=('"$(terraform | sed -rn 's/^[ ]+([^ ]+) .*/[\1]=\1/p' | tr '\n' ' ')"')' | |
;; | |
switches) | |
global_sws[dry]=sw_simple | |
;; | |
mods) | |
readarray -t all_mods< <(cd "$mods_root" ; find -maxdepth 2 -type d | sed -nr 's|^\./(.*/.*)$|\1|p' | sort) | |
;; | |
tfvar_aliases) | |
if [[ "$category" == "bundles" ]]; then | |
tfvar_aliases[node]=bundle_name | |
tfvar_aliases[name]=bundle_name | |
tfvar_aliases[domain]=bundle_name | |
else | |
tfvar_aliases[node]=guest_name | |
tfvar_aliases[name]=guest_name | |
tfvar_aliases[domain]=guest_name | |
fi | |
tfvar_aliases[ws]=workspace | |
tfvar_aliases[addr]=cm_address | |
tfvar_aliases[cm]=cm_ansible_extra_flags | |
;; | |
esac | |
done | |
} | |
# searches for category+module (using the global $category), or regexp | |
# if no matches were found return an error | |
# if one match was found, set the global category and module | |
# if multiple matches were found, present a menu. If the user selects all | |
# matches, call back the command line processing on them and exit. | |
# TODO: decople this from the control logic, just output the results | |
# | |
# PARAMS | |
# $1 <direct|regex> method to use for the search | |
# $2 string or regex to search | |
# | |
# RETURNS | |
# 0 if no errors and one match was found | |
# 1 no matches (direct method) | |
# 2 no matches (regex method) | |
# 3 no search string was provided | |
mod_select() { | |
local -a matches | |
set -f | |
local method="$1" search="$2" ; shift 2 | |
# cowardly refuse to run if the search string is not provided | |
[[ -z "$search" ]] && return 3 | |
case $method in | |
direct) readarray -t matches< <(catmodfind cat="$category" mod="$search") ;; | |
regex) readarray -t matches< <(get_elements_regex all_mods "$search") ;; | |
esac | |
set +f | |
case ${#matches[@]} in | |
0) [[ "$method" == "regex" ]] | |
return $(( $? + 1 )) | |
;; | |
1) category="$(catmodfilter cat "${matches[0]}")" | |
module="$(catmodfilter mod "${matches[0]}")" | |
return 0 | |
;; | |
*) echo "Multiple modules match '$search':" | |
for i in ${!matches[@]}; do | |
echo "$i: ${matches[$i]}" | |
done | |
msg <<EOF | |
Enter a valid index or 'a' (all) to process entries, any other input aborts. | |
"Be careful" | |
EOF | |
local i | |
read -p "selection: " i | |
case $i in | |
a) while IFS=/ read category module; do | |
main "$@" | |
done< <(printf '%s\n' "${matches[@]}") | |
exit | |
;; | |
+([[:digit:]])) | |
[[ -v matches[$i] ]] || error ENOMODSELECT | |
category="$(catmodfilter cat "${matches[i]}")" | |
module="$(catmodfilter mod "${matches[i]}")" | |
return 0 | |
;; | |
*) error ENOMODSELECT | |
;; | |
esac | |
;; | |
esac | |
} | |
main() { | |
local arg | |
[[ -z "$category" ]] && { | |
while arg="$1"; do | |
case "$arg" in | |
--@($(get_keys global_sws))) | |
${global_sws[${arg#--}]} gl $arg on | |
;; | |
@($(get_keys global_cmds))) | |
${global_cmds[$arg]} $arg $@ | |
return | |
;; | |
*) break | |
;; | |
esac | |
shift | |
done | |
# if called by "symlink alias" solving the category is easy | |
[[ -h "$0" ]] && category="${0##*/}s" | |
while arg="$1"; do | |
shift | |
case "$arg" in | |
--@($(get_keys global_sws))) | |
${global_sws[${arg#--}]} gl $arg on | |
;; | |
--@($(get_keys cat_sws))) | |
${cat_sws[${arg#--}]} cat $arg on | |
;; | |
@($(get_keys cat_cmds))) | |
[[ -z "$category" ]] && error ECMDNOCAT $arg | |
cd_relative_to_cwd "$mods_root/$category" | |
${cat_cmds[$arg]} $arg $@ | |
return | |
;; | |
# full category/module resolution - trickiest section | |
*) [[ -z "$category" ]] && { | |
category="$(catmodfind cat="$arg")" | |
[[ -n "$category" ]] && continue | |
} | |
mod_select direct "$arg" $@ || { | |
[[ -z "$category" ]] && mod_select regex "$arg" $@ | |
} | |
(( $? )) && { | |
[[ -z "$category" ]] && { | |
category="$arg" | |
error ENOCAT | |
} | |
module="$arg" | |
error ENOMOD | |
} | |
[[ -z "$module" ]] && error ENOMOD | |
break | |
;; | |
esac | |
! (( $# )) && break | |
done | |
# initialize here tfvar aliases that may depend on knowing the category | |
init tfvar_aliases | |
} | |
# remaining work (including any recursive call) done in the module context | |
cd_relative_to_cwd "$mods_root/$category/$module" | |
# Commands and flags for terraform are queued before final dispatch | |
# Note: tf_flag_queue is actually a "list of queues", holding flags per each | |
# command queue entry. Each time that a command appears a "new flag queue" | |
# is started (flag queues are just concatenated strings). | |
# To copy previous flag queues to the current one, use @ with an optional | |
# index. Ommiting the index copies the previous queue contents. | |
local -a tf_cmd_queue=() tf_flag_queue=() fqid=0 cqid=0 | |
while arg="$1"; do | |
shift | |
case "$arg" in | |
terraform) | |
continue | |
;; | |
'') break | |
;; | |
@($(get_keys tf_cmds))) | |
cqid=${#tf_cmd_queue[@]} | |
tf_cmd_queue+=( "$arg" ) | |
;; | |
\@*([[:digit:]])) | |
fqid=${arg:1} | |
(( ${fqid:=$(( cqid - 1 ))} < 0 || fqid >= cqid )) \ | |
&& error EBADFQID $fqid ${tf_cmd_queue[$cqid]} "${tf_flag_queue[$cqid]}" $arg | |
tf_flag_queue[$cqid]+=" ${tf_flag_queue[$fqid]}" | |
;; | |
\@*) | |
tf_flag_queue[$cqid]+=" $(tfvar_alias $arg)" | |
;; | |
-[^-]*) | |
tf_flag_queue[$cqid]+=" $arg" | |
;; | |
--@($(get_keys mod_sws))) | |
${mod_sws[${arg#--}]} mod $arg on | |
;; | |
@($(get_keys mod_cmds))) | |
${mod_cmds[$arg]} $@ | |
return | |
;; | |
# external commands may only come at the beginning or the very end | |
# otherwise the behaviour is not controlled | |
*) cmd_forward_shell $arg $@ | |
return | |
;; | |
esac | |
! (( $# )) && break | |
done | |
# final pass: dispatch the "pure" terraform command line | |
if (( ${#tf_cmd_queue[@]} )); then | |
for cqid in ${!tf_cmd_queue[@]}; do | |
cmd_forward_terraform ${tf_cmd_queue[$cqid]} ${tf_flag_queue[$cqid]} | |
done | |
else | |
cmd_forward_terraform -help | |
fi | |
} | |
init commands switches mods | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updates
switches system
dry run execution
clean up and improve tracking TF command/flag queues
revert to command queue length check