Created
December 29, 2021 23:24
-
-
Save ryaninvents/6d89a6d1749cf6c838be92eba5834fac to your computer and use it in GitHub Desktop.
Utility for pulling developer credentials from Google Cloud
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
#!/usr/bin/env bash | |
# @see https://github.com/xwmx/bash-boilerplate/blob/d14e016b830b1308174ba9acc461935bb45a20ed/bash-subcommands | |
set -o nounset | |
set -o errexit | |
trap 'echo "Aborting due to errexit on line $LINENO. Exit code: $?" >&2' ERR | |
set -o errtrace | |
set -o pipefail | |
IFS=$'\n\t' | |
############################################################################### | |
# Globals | |
############################################################################### | |
# $_ME | |
# | |
# This program's basename. | |
_ME="$(basename "${0}")" | |
# $_VERSION | |
# | |
# Manually set this to to current version of the program. Adhere to the | |
# semantic versioning specification: http://semver.org | |
_VERSION="0.0.0-rolling" | |
# $DEFAULT_SUBCOMMAND | |
# | |
# The subcommand to be run by default, when no subcommand name is specified. | |
# If the environment has an existing $DEFAULT_SUBCOMMAND set, then that value | |
# is used. | |
DEFAULT_SUBCOMMAND="${DEFAULT_SUBCOMMAND:-help}" | |
# $_BUCKET_PREFIX | |
# | |
# The Google Storage URL to the root location for your secrets. Secrets are expected to be organized | |
# with one top-level folder for each repo, and each repo's folder corresponding to the code | |
# structure for that project. | |
# | |
# Sample value: "gs://developer-secrets/repo/" | |
# | |
# Example: | |
# | |
# repo/ | |
# project-1/ | |
# .env.target.staging # staging environment for project root | |
# .env.target.production # production environment for project root | |
# packages/ # assume that `project-1` is a monorepo | |
# www/ | |
# .env.target.staging # staging environment for WWW package | |
# .env.target.production # production environment for WWW package | |
# project-2/ | |
# .env.target.staging # staging environment for project root | |
# .env.target.production # production environment for project root | |
# | |
_BUCKET_PREFIX="${DEVCREDS_BUCKET_PREFIX}" | |
_GCP_PROJECT="${DEVCREDS_GCP_PROJECT}" | |
# __get_option_value() | |
# | |
# Usage: | |
# __get_option_value <option> <value> | |
# | |
# Description: | |
# Given a flag (e.g., -e | --example) return the value or exit 1 if value | |
# is blank or appears to be another option. | |
__get_option_value() { | |
local __arg="${1:-}" | |
local __val="${2:-}" | |
if [[ -n "${__val:-}" ]] && [[ ! "${__val:-}" =~ ^- ]] | |
then | |
printf "%s\\n" "${__val}" | |
else | |
_exit_1 printf "%s requires a valid argument.\\n" "${__arg}" | |
fi | |
} | |
############################################################################### | |
# Debug | |
############################################################################### | |
# _debug() | |
# | |
# Usage: | |
# _debug <command> <options>... | |
# | |
# Description: | |
# Execute a command and print to standard error. The command is expected to | |
# print a message and should typically be either `echo`, `printf`, or `cat`. | |
# | |
# Example: | |
# _debug printf "Debug info. Variable: %s\\n" "$0" | |
__DEBUG_COUNTER=0 | |
_debug() { | |
if ((${_USE_DEBUG:-0})) | |
then | |
__DEBUG_COUNTER=$((__DEBUG_COUNTER+1)) | |
{ | |
# Prefix debug message with "bug (U+1F41B)" | |
printf "🐛 %s " "${__DEBUG_COUNTER}" | |
"${@}" | |
printf "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n" | |
} 1>&2 | |
fi | |
} | |
############################################################################### | |
# Error Messages | |
############################################################################### | |
# _exit_1() | |
# | |
# Usage: | |
# _exit_1 <command> | |
# | |
# Description: | |
# Exit with status 1 after executing the specified command with output | |
# redirected to standard error. The command is expected to print a message | |
# and should typically be either `echo`, `printf`, or `cat`. | |
_exit_1() { | |
{ | |
printf "%s " "$(tput setaf 1)!$(tput sgr0)" | |
"${@}" | |
} 1>&2 | |
exit 1 | |
} | |
# _warn() | |
# | |
# Usage: | |
# _warn <command> | |
# | |
# Description: | |
# Print the specified command with output redirected to standard error. | |
# The command is expected to print a message and should typically be either | |
# `echo`, `printf`, or `cat`. | |
_warn() { | |
{ | |
printf "%s " "$(tput setaf 1)!$(tput sgr0)" | |
"${@}" | |
} 1>&2 | |
} | |
############################################################################### | |
# Utility Functions | |
############################################################################### | |
# _function_exists() | |
# | |
# Usage: | |
# _function_exists <name> | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If function with <name> is defined in the current | |
# environment. | |
# 1 (error, false) If not. | |
# | |
# Other implementations, some with better performance: | |
# http://stackoverflow.com/q/85880 | |
_function_exists() { | |
[ "$(type -t "${1}")" == 'function' ] | |
} | |
# _command_exists() | |
# | |
# Usage: | |
# _command_exists <name> | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If a command with <name> is defined in the current | |
# environment. | |
# 1 (error, false) If not. | |
# | |
# Information on why `hash` is used here: | |
# http://stackoverflow.com/a/677212 | |
_command_exists() { | |
hash "${1}" 2>/dev/null | |
} | |
# _contains() | |
# | |
# Usage: | |
# _contains <query> <list-item>... | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If the item is included in the list. | |
# 1 (error, false) If not. | |
# | |
# Examples: | |
# _contains "${_query}" "${_list[@]}" | |
_contains() { | |
local _query="${1:-}" | |
shift | |
if [[ -z "${_query}" ]] || | |
[[ -z "${*:-}" ]] | |
then | |
return 1 | |
fi | |
for __element in "${@}" | |
do | |
[[ "${__element}" == "${_query}" ]] && return 0 | |
done | |
return 1 | |
} | |
# _join() | |
# | |
# Usage: | |
# _join <delimiter> <list-item>... | |
# | |
# Description: | |
# Print a string containing all <list-item> arguments separated by | |
# <delimeter>. | |
# | |
# Example: | |
# _join "${_delimeter}" "${_list[@]}" | |
# | |
# More information: | |
# https://stackoverflow.com/a/17841619 | |
_join() { | |
local _delimiter="${1}" | |
shift | |
printf "%s" "${1}" | |
shift | |
printf "%s" "${@/#/${_delimiter}}" | tr -d '[:space:]' | |
} | |
# _blank() | |
# | |
# Usage: | |
# _blank <argument> | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If <argument> is not present or null. | |
# 1 (error, false) If <argument> is present and not null. | |
_blank() { | |
[[ -z "${1:-}" ]] | |
} | |
# _present() | |
# | |
# Usage: | |
# _present <argument> | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If <argument> is present and not null. | |
# 1 (error, false) If <argument> is not present or null. | |
_present() { | |
[[ -n "${1:-}" ]] | |
} | |
# _interactive_input() | |
# | |
# Usage: | |
# _interactive_input | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If the current input is interactive (eg, a shell). | |
# 1 (error, false) If the current input is stdin / piped input. | |
_interactive_input() { | |
[[ -t 0 ]] | |
} | |
# _piped_input() | |
# | |
# Usage: | |
# _piped_input | |
# | |
# Exit / Error Status: | |
# 0 (success, true) If the current input is stdin / piped input. | |
# 1 (error, false) If the current input is interactive (eg, a shell). | |
_piped_input() { | |
! _interactive_input | |
} | |
############################################################################### | |
# describe | |
############################################################################### | |
# describe() | |
# | |
# Usage: | |
# describe <name> <description> | |
# describe --get <name> | |
# | |
# Options: | |
# --get Print the description for <name> if one has been set. | |
# | |
# Examples: | |
# ``` | |
# describe "list" <<HEREDOC | |
# Usage: | |
# ${_ME} list | |
# | |
# Description: | |
# List items. | |
# HEREDOC | |
# | |
# describe --get "list" | |
# ``` | |
# | |
# Set or print a description for a specified subcommand or function <name>. The | |
# <description> text can be passed as the second argument or as standard input. | |
# | |
# To make the <description> text available to other functions, `describe()` | |
# assigns the text to a variable with the format `$___describe_<name>`. | |
# | |
# When the `--get` option is used, the description for <name> is printed, if | |
# one has been set. | |
# | |
# NOTE: | |
# | |
# The `read` form of assignment is used for a balance of ease of | |
# implementation and simplicity. There is an alternative assignment form | |
# that could be used here: | |
# | |
# var="$(cat <<'HEREDOC' | |
# some message | |
# HEREDOC | |
# ) | |
# | |
# However, this form appears to require trailing space after backslases to | |
# preserve newlines, which is unexpected. Using `read` simply requires | |
# escaping backslashes, which is more common. | |
describe() { | |
_debug printf "describe() \${*}: %s\\n" "$@" | |
[[ -z "${1:-}" ]] && _exit_1 printf "describe(): <name> required.\\n" | |
if [[ "${1}" == "--get" ]] | |
then # get ------------------------------------------------------------------ | |
[[ -z "${2:-}" ]] && | |
_exit_1 printf "describe(): <description> required.\\n" | |
local _name="${2:-}" | |
local _describe_var="___describe_${_name}" | |
if [[ -n "${!_describe_var:-}" ]] | |
then | |
printf "%s\\n" "${!_describe_var}" | |
else | |
printf "No additional information for \`%s\`\\n" "${_name}" | |
fi | |
else # set ------------------------------------------------------------------ | |
if [[ -n "${2:-}" ]] | |
then # argument is present | |
read -r -d '' "___describe_${1}" <<HEREDOC | |
${2} | |
HEREDOC | |
else # no argument is present, so assume piped input | |
# `read` exits with non-zero status when a delimeter is not found, so | |
# avoid errors by ending statement with `|| true`. | |
read -r -d '' "___describe_${1}" || true | |
fi | |
fi | |
} | |
############################################################################### | |
# Program Option Parsing | |
# | |
# NOTE: The `getops` builtin command only parses short options and BSD `getopt` | |
# does not support long arguments (GNU `getopt` does), so use custom option | |
# normalization and parsing. | |
# | |
# For a pure bash `getopt` function, try pure-getopt: | |
# https://github.com/agriffis/pure-getopt | |
# | |
# More info: | |
# http://wiki.bash-hackers.org/scripting/posparams | |
# http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html | |
# http://stackoverflow.com/a/14203146 | |
# http://stackoverflow.com/a/7948533 | |
# https://stackoverflow.com/a/12026302 | |
# https://stackoverflow.com/a/402410 | |
############################################################################### | |
# Normalize Options ########################################################### | |
# Source: | |
# https://github.com/e36freak/templates/blob/master/options | |
# Iterate over options, breaking -ab into -a -b and --foo=bar into --foo bar | |
# also turns -- into --endopts to avoid issues with things like '-o-', the '-' | |
# should not indicate the end of options, but be an invalid option (or the | |
# argument to the option, such as wget -qO-) | |
unset options | |
# while the number of arguments is greater than 0 | |
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 | |
# extract 1 character from position 'i' | |
c="${1:i:1}" | |
# add current char to options | |
options+=("-${c}") | |
done | |
;; | |
# if option is of type --foo=bar, split on first '=' | |
--?*=*) | |
options+=("${1%%=*}" "${1#*=}") | |
;; | |
# end of options, stop breaking them up | |
--) | |
options+=(--endopts) | |
shift | |
options+=("${@}") | |
break | |
;; | |
# otherwise, nothing special | |
*) | |
options+=("${1}") | |
;; | |
esac | |
shift | |
done | |
# set new positional parameters to altered options. Set default to blank. | |
set -- "${options[@]:-}" | |
unset options | |
# Parse Options ############################################################### | |
_SUBCOMMAND="" | |
_SUBCOMMAND_ARGUMENTS=() | |
_USE_DEBUG=0 | |
while ((${#})) | |
do | |
__opt="${1}" | |
shift | |
case "${__opt}" in | |
-h|--help) | |
_SUBCOMMAND="help" | |
;; | |
--version) | |
_SUBCOMMAND="version" | |
;; | |
--debug) | |
_USE_DEBUG=1 | |
;; | |
*) | |
# The first non-option argument is assumed to be the subcommand name. | |
# All subsequent arguments are added to $_SUBCOMMAND_ARGUMENTS. | |
if [[ -n "${_SUBCOMMAND}" ]] | |
then | |
_SUBCOMMAND_ARGUMENTS+=("${__opt}") | |
else | |
_SUBCOMMAND="${__opt}" | |
fi | |
;; | |
esac | |
done | |
############################################################################### | |
# Main | |
############################################################################### | |
# Declare the $_DEFINED_SUBCOMMANDS array. | |
_DEFINED_SUBCOMMANDS=() | |
# _main() | |
# | |
# Usage: | |
# _main | |
# | |
# Description: | |
# The primary function for starting the program. | |
# | |
# NOTE: must be called at end of program after all subcommands are defined. | |
_main() { | |
# If $_SUBCOMMAND is blank, then set to `$DEFAULT_SUBCOMMAND` | |
if [[ -z "${_SUBCOMMAND}" ]] | |
then | |
_SUBCOMMAND="${DEFAULT_SUBCOMMAND}" | |
fi | |
for __name in $(declare -F) | |
do | |
# Each element has the format `declare -f function_name`, so set the name | |
# to only the 'function_name' part of the string. | |
local _function_name | |
_function_name=$(printf "%s" "${__name}" | awk '{ print $3 }') | |
if ! { [[ -z "${_function_name:-}" ]] || | |
[[ "${_function_name}" =~ ^_(.*) ]] || | |
[[ "${_function_name}" == "bats_readlinkf" ]] || | |
[[ "${_function_name}" == "describe" ]] || | |
[[ "${_function_name}" == "shell_session_update" ]] | |
} | |
then | |
_DEFINED_SUBCOMMANDS+=("${_function_name}") | |
fi | |
done | |
# If the subcommand is defined, run it, otherwise return an error. | |
if _contains "${_SUBCOMMAND}" "${_DEFINED_SUBCOMMANDS[@]:-}" | |
then | |
# Pass all comment arguments to the program except for the first ($0). | |
${_SUBCOMMAND} "${_SUBCOMMAND_ARGUMENTS[@]:-}" | |
else | |
_exit_1 printf "Unknown subcommand: %s\\n" "${_SUBCOMMAND}" | |
fi | |
} | |
############################################################################### | |
# Default Subcommands | |
############################################################################### | |
# help ######################################################################## | |
describe "help" <<HEREDOC | |
Usage: | |
${_ME} help [<subcommand>] | |
Description: | |
Display help information for ${_ME} or a specified subcommand. | |
HEREDOC | |
help() { | |
if [[ "${1:-}" ]] | |
then | |
describe --get "${1}" | |
else | |
cat <<HEREDOC | |
Credential helper for quickly pulling down credentials from a Google Cloud bucket. | |
Version: ${_VERSION} | |
Usage: | |
${_ME} <subcommand> [--subcommand-options] [<arguments>] | |
${_ME} -h | --help | |
${_ME} --version | |
Options: | |
-h --help Display this help information. | |
--version Display version information. | |
Help: | |
${_ME} help [<subcommand>] | |
$(subcommands --) | |
HEREDOC | |
fi | |
} | |
# subcommands ################################################################# | |
describe "subcommands" <<HEREDOC | |
Usage: | |
${_ME} subcommands [--raw] | |
Options: | |
--raw Display the subcommand list without formatting. | |
Description: | |
Display the list of available subcommands. | |
HEREDOC | |
subcommands() { | |
if [[ "${1:-}" == "--raw" ]] | |
then | |
printf "%s\\n" "${_DEFINED_SUBCOMMANDS[@]}" | |
else | |
printf "Available subcommands:\\n" | |
printf " %s\\n" "${_DEFINED_SUBCOMMANDS[@]}" | |
fi | |
} | |
# version ##################################################################### | |
describe "version" <<HEREDOC | |
Usage: | |
${_ME} ( version | --version ) | |
Description: | |
Display the current program version. | |
To save you the trouble, the current version is ${_VERSION} | |
HEREDOC | |
version() { | |
printf "%s\\n" "${_VERSION}" | |
} | |
############################################################################### | |
# Subcommands | |
# ===========.................................................................. | |
# | |
# Example subcommand group structure: | |
# | |
# describe example "" - Optional. A short description for the subcommand. | |
# example() { : } - The subcommand called by the user. | |
# | |
# | |
# describe example <<HEREDOC | |
# Usage: | |
# $_ME example | |
# | |
# Description: | |
# Print "Hello, World!" | |
# | |
# For usage formatting conventions see: | |
# - http://docopt.org/ | |
# - http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html | |
# HEREDOC | |
# example() { | |
# printf "Hello, World!\\n" | |
# } | |
# | |
############################################################################### | |
# ------------------------------------------------------------------------ login | |
describe "login" <<HEREDOC | |
Sign into the Google Cloud account containing our developer credentials. | |
Usage: | |
${_ME} login | |
HEREDOC | |
login() { | |
gcloud init --project "${_GCP_PROJECT}" | |
} | |
# ----------------------------------------------------------------------- repos | |
describe "repos" <<HEREDOC | |
HEREDOC | |
repos() { | |
gsutil ls -d "${_BUCKET_PREFIX}*" | cut -c$((${#_BUCKET_PREFIX}+1))- | rev | cut -c2- | rev | |
} | |
# ------------------------------------------------------------------------ pull | |
describe "pull" <<HEREDOC | |
Download credentials. Run ${_ME} repos to get a list of available repos. | |
Usage: | |
${_ME} pull --repo <repo_name> | |
HEREDOC | |
pull() { | |
local _arguments=() | |
local _repo= | |
for __arg in "${@:-}" | |
do | |
case ${__arg} in | |
--repo) | |
_repo="${__arg}" | |
;; | |
-*) | |
_exit_1 printf "Unexpected option: %s\\n" "${__arg}" | |
;; | |
esac | |
done | |
if [[ -z "${_repo}" ]] | |
then | |
_exit_1 printf "Missing required option --repo\\nTry ${_ME} help pull for details" | |
fi | |
gsutil cp "${_BUCKET_PREFIX}${_repo}/$(git rev-parse --show-prefix)*" . | |
} | |
# ------------------------------------------------------------------------ push | |
describe "push" <<HEREDOC | |
Download credentials. Run ${_ME} repos to get a list of available repos. | |
Usage: | |
${_ME} push --repo <repo_name> <filename> | |
Options: | |
--repo <repo_name> Select repo (required) | |
--dry-run Print planned execution | |
Example: | |
${_ME} push --repo api ./.env.target.staging | |
HEREDOC | |
push() { | |
local _repo= | |
local _filenames=() | |
local _dry_run= | |
while ((${#})) | |
do | |
__arg="${1:-}" | |
__val="${2:-}" | |
case "${__arg}" in | |
--repo) | |
_repo="${__val}" | |
shift | |
;; | |
--dry-run) | |
_dry_run="yes" | |
;; | |
-*) | |
_exit_1 printf "Unexpected option: %s\\n" "${__arg}" | |
;; | |
*) | |
_filenames+="${__arg} " | |
;; | |
esac | |
shift | |
done | |
if [[ -z "${_repo}" ]] | |
then | |
_exit_1 printf "Missing required option --repo\\nTry ${_ME} help pull for details" | |
fi | |
if [[ -n "${_dry_run}" ]] | |
then | |
echo cp "${_filenames}" "${_BUCKET_PREFIX}${_repo}/$(git rev-parse --show-prefix)" | |
exit 0 | |
fi | |
gsutil cp "${_filenames}" "${_BUCKET_PREFIX}${_repo}/$(git rev-parse --show-prefix)" | |
} | |
# ------------------------------------------------------------------------ browse | |
describe "browse" <<HEREDOC | |
Get a browser link to view .env files directly in the Google Cloud console. | |
Usage: | |
${_ME} browse | |
HEREDOC | |
browse() { | |
echo "https://console.cloud.google.com/storage/browser/$(echo $_BUCKET_PREFIX | cut -c6-)" | |
} | |
# ------------------------------------------------------------------------ pull | |
describe "pull" <<HEREDOC | |
Download credentials. Run ${_ME} repos to get a list of available repos. | |
Usage: | |
${_ME} pull --repo <repo_name> | |
HEREDOC | |
pull() { | |
local _arguments=() | |
local _repo= | |
for __arg in "${@:-}" | |
do | |
case ${__arg} in | |
--repo) | |
_repo="${__arg}" | |
;; | |
-*) | |
_exit_1 printf "Unexpected option: %s\\n" "${__arg}" | |
;; | |
esac | |
done | |
if [[ -z "${_repo}" ]] | |
then | |
_exit_1 printf "Missing required option --repo\\nTry ${_ME} help pull for details" | |
fi | |
gsutil cp "${_BUCKET_PREFIX}${_repo}/$(git rev-parse --show-prefix)*" . | |
} | |
# ------------------------------------------------------------------------ link | |
describe "link" <<HEREDOC | |
Symlink a local .env file | |
Usage: | |
${_ME} link <target> <link_name> | |
Example: | |
# Symlink .env.local -> .env.target.staging | |
${_ME} link .env.target.staging | |
# Symlink .env.development -> .env.target.staging | |
${_ME} link .env.target.staging .env.development | |
HEREDOC | |
link() { | |
local _target= | |
local _link_name=".env.local" | |
while ((${#})) | |
do | |
__arg="${1:-}" | |
__val="${2:-}" | |
case "${__arg}" in | |
-*) | |
_exit_1 printf "Unexpected option: %s\\n" "${__arg}" | |
;; | |
*) | |
if [[ -z "${_target}" ]] | |
then | |
_target="${__arg}" | |
else | |
_link_name="${__arg}" | |
fi | |
;; | |
esac | |
shift | |
done | |
rm -f $_link_name | |
ln -s $_target $_link_name | |
} | |
############################################################################### | |
# Run Program | |
############################################################################### | |
# Call the `_main` function after everything has been defined. | |
_main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment