diff --git a/abra b/abra index 8966eb6..e269e25 100755 --- a/abra +++ b/abra @@ -1,25 +1,29 @@ #!/usr/bin/env bash +GIT_URL="https://git.autonomic.zone/coop-cloud/" +ABRA_DIR="$HOME/.abra" + ###### Global help DOC=" The cooperative cloud utility belt 🎩🐇 Usage: - abra [options] deploy - abra [options] upgrade - abra [options] logs [] - abra [options] multilogs - abra [options] cp - abra [options] run [--user=] ... - abra [options] run ... - abra [options] secret generate [] - abra [options] secret insert + abra [options] new [--server= --domain=] + abra [options] deploy + abra [options] logs [] + abra [options] multilogs + abra [options] cp + abra [options] run [--user=] ... + abra [options] run ... + abra [options] secret generate [] + abra [options] secret insert abra [options] server add [] [] abra [options] server rm abra [options] server use abra [options] server init abra [options] server ls + abra [options] upgrade abra [options] ... Options: @@ -28,6 +32,8 @@ Options: -h, --help Show this message and exit -s, --stack= Name of the target stack -v, --version Show program version + +See 'abra help ' to read about a specific subcommand. " # docopt parser below, refresh this parser with `docopt.sh abra` @@ -129,59 +135,65 @@ eval "var_$1+=($value)"; else eval "var_$1=$value"; fi; return 0; fi; done return 1; }; stdout() { printf -- "cat <<'EOM'\n%s\nEOM\n" "$1"; }; stderr() { printf -- "cat <<'EOM' >&2\n%s\nEOM\n" "$1"; }; error() { [[ -n $1 ]] && stderr "$1"; stderr "$usage"; _return 1; }; _return() { -printf -- "exit %d\n" "$1"; exit "$1"; }; set -e; trimmed_doc=${DOC:1:871} -usage=${DOC:40:577}; digest=d484a; shorts=(-v -c -s -e -h '') -longs=(--version --config --stack --env --help --user); argcounts=(0 1 1 1 0 1) -node_0(){ switch __version 0; }; node_1(){ value __config 1; }; node_2(){ -value __stack 2; }; node_3(){ value __env 3; }; node_4(){ switch __help 4; } -node_5(){ value __user 5; }; node_6(){ value _service_ a; }; node_7(){ -value _src_ a; }; node_8(){ value _dst_ a; }; node_9(){ value _app_ a; } -node_10(){ value _args_ a true; }; node_11(){ value _secret_ a; }; node_12(){ -value _version_ a; }; node_13(){ value _cmd_ a; }; node_14(){ value _data_ a; } -node_15(){ value _host_ a; }; node_16(){ value _user_ a; }; node_17(){ -value _port_ a; }; node_18(){ value _command_ a; }; node_19(){ _command deploy -}; node_20(){ _command upgrade; }; node_21(){ _command logs; }; node_22(){ -_command multilogs; }; node_23(){ _command cp; }; node_24(){ _command run; } -node_25(){ _command secret; }; node_26(){ _command generate; }; node_27(){ -_command insert; }; node_28(){ _command server; }; node_29(){ _command add; } -node_30(){ _command rm; }; node_31(){ _command use; }; node_32(){ _command init -}; node_33(){ _command ls; }; node_34(){ optional 0 1 2 3 4; }; node_35(){ -optional 34; }; node_36(){ required 35 19; }; node_37(){ required 35 20; } -node_38(){ optional 6; }; node_39(){ required 35 21 38; }; node_40(){ -required 35 22; }; node_41(){ required 35 23 7 8; }; node_42(){ optional 5; } -node_43(){ oneormore 10; }; node_44(){ required 35 24 42 9 43; }; node_45(){ -required 35 24 9 43; }; node_46(){ optional 13; }; node_47(){ -required 35 25 26 11 12 46; }; node_48(){ required 35 25 27 11 12 14; } -node_49(){ optional 16; }; node_50(){ optional 17; }; node_51(){ -required 35 28 29 15 49 50; }; node_52(){ required 35 28 30 15; }; node_53(){ -required 35 28 31 15; }; node_54(){ required 35 28 32 15; }; node_55(){ -required 35 28 33; }; node_56(){ required 35 18 43; }; node_57(){ -either 36 37 39 40 41 44 45 47 48 51 52 53 54 55 56; }; node_58(){ required 57 -}; cat <<<' docopt_exit() { [[ -n $1 ]] && printf "%s\n" "$1" >&2 -printf "%s\n" "${DOC:40:577}" >&2; exit 1; }'; unset var___version \ -var___config var___stack var___env var___help var___user var__service_ \ -var__src_ var__dst_ var__app_ var__args_ var__secret_ var__version_ var__cmd_ \ -var__data_ var__host_ var__user_ var__port_ var__command_ var_deploy \ -var_upgrade var_logs var_multilogs var_cp var_run var_secret var_generate \ -var_insert var_server var_add var_rm var_use var_init var_ls; parse 58 "$@" -local prefix=${DOCOPT_PREFIX:-''}; unset "${prefix}__version" \ -"${prefix}__config" "${prefix}__stack" "${prefix}__env" "${prefix}__help" \ -"${prefix}__user" "${prefix}_service_" "${prefix}_src_" "${prefix}_dst_" \ -"${prefix}_app_" "${prefix}_args_" "${prefix}_secret_" "${prefix}_version_" \ -"${prefix}_cmd_" "${prefix}_data_" "${prefix}_host_" "${prefix}_user_" \ -"${prefix}_port_" "${prefix}_command_" "${prefix}deploy" "${prefix}upgrade" \ -"${prefix}logs" "${prefix}multilogs" "${prefix}cp" "${prefix}run" \ -"${prefix}secret" "${prefix}generate" "${prefix}insert" "${prefix}server" \ -"${prefix}add" "${prefix}rm" "${prefix}use" "${prefix}init" "${prefix}ls" -eval "${prefix}"'__version=${var___version:-false}' +printf -- "exit %d\n" "$1"; exit "$1"; }; set -e; trimmed_doc=${DOC:1:1080} +usage=${DOC:40:722}; digest=14148; shorts=(-h -c -s -e -v '' '' '') +longs=(--help --config --stack --env --version --server --domain --user) +argcounts=(0 1 1 1 0 1 1 1); node_0(){ switch __help 0; }; node_1(){ +value __config 1; }; node_2(){ value __stack 2; }; node_3(){ value __env 3; } +node_4(){ switch __version 4; }; node_5(){ value __server 5; }; node_6(){ +value __domain 6; }; node_7(){ value __user 7; }; node_8(){ value _app_ a; } +node_9(){ value _domain_ a; }; node_10(){ value _service_ a; }; node_11(){ +value _src_ a; }; node_12(){ value _dst_ a; }; node_13(){ value _args_ a true; } +node_14(){ value _secret_ a; }; node_15(){ value _version_ a; }; node_16(){ +value _cmd_ a; }; node_17(){ value _data_ a; }; node_18(){ value _host_ a; } +node_19(){ value _user_ a; }; node_20(){ value _port_ a; }; node_21(){ +value _command_ a; }; node_22(){ _command new; }; node_23(){ _command deploy; } +node_24(){ _command logs; }; node_25(){ _command multilogs; }; node_26(){ +_command cp; }; node_27(){ _command run; }; node_28(){ _command secret; } +node_29(){ _command generate; }; node_30(){ _command insert; }; node_31(){ +_command server; }; node_32(){ _command add; }; node_33(){ _command rm; } +node_34(){ _command use; }; node_35(){ _command init; }; node_36(){ _command ls +}; node_37(){ _command upgrade; }; node_38(){ optional 0 1 2 3 4; }; node_39(){ +optional 38; }; node_40(){ optional 5 6; }; node_41(){ required 39 22 40 8; } +node_42(){ required 39 9 23; }; node_43(){ optional 10; }; node_44(){ +required 39 9 24 43; }; node_45(){ required 39 9 25; }; node_46(){ +required 39 9 26 11 12; }; node_47(){ optional 7; }; node_48(){ oneormore 13; } +node_49(){ required 39 9 27 47 10 48; }; node_50(){ required 39 9 27 10 48; } +node_51(){ optional 16; }; node_52(){ required 39 9 28 29 14 15 51; } +node_53(){ required 39 9 28 30 14 15 17; }; node_54(){ optional 19; } +node_55(){ optional 20; }; node_56(){ required 39 31 32 18 54 55; }; node_57(){ +required 39 31 33 18; }; node_58(){ required 39 31 34 18; }; node_59(){ +required 39 31 35 18; }; node_60(){ required 39 31 36; }; node_61(){ +required 39 37; }; node_62(){ required 39 21 48; }; node_63(){ +either 41 42 44 45 46 49 50 52 53 56 57 58 59 60 61 62; }; node_64(){ +required 63; }; cat <<<' docopt_exit() { [[ -n $1 ]] && printf "%s\n" "$1" >&2 +printf "%s\n" "${DOC:40:722}" >&2; exit 1; }'; unset var___help var___config \ +var___stack var___env var___version var___server var___domain var___user \ +var__app_ var__domain_ var__service_ var__src_ var__dst_ var__args_ \ +var__secret_ var__version_ var__cmd_ var__data_ var__host_ var__user_ \ +var__port_ var__command_ var_new var_deploy var_logs var_multilogs var_cp \ +var_run var_secret var_generate var_insert var_server var_add var_rm var_use \ +var_init var_ls var_upgrade; parse 64 "$@"; local prefix=${DOCOPT_PREFIX:-''} +unset "${prefix}__help" "${prefix}__config" "${prefix}__stack" \ +"${prefix}__env" "${prefix}__version" "${prefix}__server" "${prefix}__domain" \ +"${prefix}__user" "${prefix}_app_" "${prefix}_domain_" "${prefix}_service_" \ +"${prefix}_src_" "${prefix}_dst_" "${prefix}_args_" "${prefix}_secret_" \ +"${prefix}_version_" "${prefix}_cmd_" "${prefix}_data_" "${prefix}_host_" \ +"${prefix}_user_" "${prefix}_port_" "${prefix}_command_" "${prefix}new" \ +"${prefix}deploy" "${prefix}logs" "${prefix}multilogs" "${prefix}cp" \ +"${prefix}run" "${prefix}secret" "${prefix}generate" "${prefix}insert" \ +"${prefix}server" "${prefix}add" "${prefix}rm" "${prefix}use" "${prefix}init" \ +"${prefix}ls" "${prefix}upgrade"; eval "${prefix}"'__help=${var___help:-false}' eval "${prefix}"'__config=${var___config:-}' eval "${prefix}"'__stack=${var___stack:-}' eval "${prefix}"'__env=${var___env:-}' -eval "${prefix}"'__help=${var___help:-false}' -eval "${prefix}"'__user=${var___user:-}' +eval "${prefix}"'__version=${var___version:-false}' +eval "${prefix}"'__server=${var___server:-}' +eval "${prefix}"'__domain=${var___domain:-}' +eval "${prefix}"'__user=${var___user:-}'; eval "${prefix}"'_app_=${var__app_:-}' +eval "${prefix}"'_domain_=${var__domain_:-}' eval "${prefix}"'_service_=${var__service_:-}' eval "${prefix}"'_src_=${var__src_:-}'; eval "${prefix}"'_dst_=${var__dst_:-}' -eval "${prefix}"'_app_=${var__app_:-}' if declare -p var__args_ >/dev/null 2>&1; then eval "${prefix}"'_args_=("${var__args_[@]}")'; else eval "${prefix}"'_args_=()' fi; eval "${prefix}"'_secret_=${var__secret_:-}' @@ -191,8 +203,8 @@ eval "${prefix}"'_host_=${var__host_:-}' eval "${prefix}"'_user_=${var__user_:-}' eval "${prefix}"'_port_=${var__port_:-}' eval "${prefix}"'_command_=${var__command_:-}' +eval "${prefix}"'new=${var_new:-false}' eval "${prefix}"'deploy=${var_deploy:-false}' -eval "${prefix}"'upgrade=${var_upgrade:-false}' eval "${prefix}"'logs=${var_logs:-false}' eval "${prefix}"'multilogs=${var_multilogs:-false}' eval "${prefix}"'cp=${var_cp:-false}'; eval "${prefix}"'run=${var_run:-false}' @@ -203,17 +215,18 @@ eval "${prefix}"'server=${var_server:-false}' eval "${prefix}"'add=${var_add:-false}'; eval "${prefix}"'rm=${var_rm:-false}' eval "${prefix}"'use=${var_use:-false}' eval "${prefix}"'init=${var_init:-false}'; eval "${prefix}"'ls=${var_ls:-false}' -local docopt_i=1; [[ $BASH_VERSION =~ ^4.3 ]] && docopt_i=2 -for ((;docopt_i>0;docopt_i--)); do declare -p "${prefix}__version" \ -"${prefix}__config" "${prefix}__stack" "${prefix}__env" "${prefix}__help" \ -"${prefix}__user" "${prefix}_service_" "${prefix}_src_" "${prefix}_dst_" \ -"${prefix}_app_" "${prefix}_args_" "${prefix}_secret_" "${prefix}_version_" \ -"${prefix}_cmd_" "${prefix}_data_" "${prefix}_host_" "${prefix}_user_" \ -"${prefix}_port_" "${prefix}_command_" "${prefix}deploy" "${prefix}upgrade" \ -"${prefix}logs" "${prefix}multilogs" "${prefix}cp" "${prefix}run" \ -"${prefix}secret" "${prefix}generate" "${prefix}insert" "${prefix}server" \ -"${prefix}add" "${prefix}rm" "${prefix}use" "${prefix}init" "${prefix}ls"; done -} +eval "${prefix}"'upgrade=${var_upgrade:-false}'; local docopt_i=1 +[[ $BASH_VERSION =~ ^4.3 ]] && docopt_i=2; for ((;docopt_i>0;docopt_i--)); do +declare -p "${prefix}__help" "${prefix}__config" "${prefix}__stack" \ +"${prefix}__env" "${prefix}__version" "${prefix}__server" "${prefix}__domain" \ +"${prefix}__user" "${prefix}_app_" "${prefix}_domain_" "${prefix}_service_" \ +"${prefix}_src_" "${prefix}_dst_" "${prefix}_args_" "${prefix}_secret_" \ +"${prefix}_version_" "${prefix}_cmd_" "${prefix}_data_" "${prefix}_host_" \ +"${prefix}_user_" "${prefix}_port_" "${prefix}_command_" "${prefix}new" \ +"${prefix}deploy" "${prefix}logs" "${prefix}multilogs" "${prefix}cp" \ +"${prefix}run" "${prefix}secret" "${prefix}generate" "${prefix}insert" \ +"${prefix}server" "${prefix}add" "${prefix}rm" "${prefix}use" "${prefix}init" \ +"${prefix}ls" "${prefix}upgrade"; done; } # docopt parser above, complete command for generating this parser is `docopt.sh abra` PROGRAM_NAME=$(basename "$0") @@ -251,6 +264,10 @@ require_multitail() { fi } +require_abra_dir() { + mkdir -p "$ABRA_DIR" +} + require_stack() { if [ -z "$STACK_NAME" ]; then error "no stack_name, export \$STACK_NAME=my_cool_app" @@ -258,11 +275,13 @@ require_stack() { } require_stack_dir() { + # FIXME 3wc: replace with APP_DIR if [ -z "$ABRA_STACK_DIR" ] || [ ! -d "$ABRA_STACK_DIR" ]; then error "can't find \$ABRA_STACK_DIR '$ABRA_STACK_DIR'" fi } +# FIXME 3wc: update or remove if [ -z "$ABRA_ENV" ] && [ -f .envrc ] && type direnv > /dev/null 2>&1 && ! direnv status | grep -q 'Found RC allowed true'; then error "direnv is blocked, run direnv allow" fi @@ -281,6 +300,29 @@ load_custom_commands() { fi } +load_instance() { + # FIXME 3wc: try and support old behaviour of just specifying -a STACK + DOMAIN="$abra__domain_" + ENV_FILE=$(find ~/.abra -name "$DOMAIN.env") + if [ "$(echo "$ENV_FILE" | wc -l)" -gt 1 ]; then + error "Found $DOMAIN in multiple servers: $ENV_FILE" + fi + IFS='/' read -r -a PARTS <<< "$ENV_FILE" + SERVER="${PARTS[-2]}" + export STACK_NAME="${DOMAIN//./_}" +} + +load_instance_env() { + # shellcheck disable=SC1090 + source "$ENV_FILE" + if [ -z "$APP" ]; then + error "APP not set, maybe $ENV_FILE is using an old format?" + fi + APP_DIR="$ABRA_DIR/apps/$APP" + export DOCKER_CONTEXT="$SERVER" + export DOMAIN +} + load_context() { # Load current context from env or Docker if [ -z "$DOCKER_CONTEXT" ]; then @@ -294,8 +336,8 @@ load_context() { ###### Subcommand `secret` sub_secret_insert() { - require_stack - load_context + load_instance + load_instance_env SECRET="$abra__secret_" VERSION="$abra__version_" @@ -313,15 +355,16 @@ sub_secret_insert() { } sub_secret_generate(){ + load_instance + load_instance_env + SECRET="$abra__secret_" VERSION="$abra__version_" PWGEN=${abra__cmd_:-pwqgen} + if [ -z "$SECRET" ] || [ -z "$VERSION" ]; then error "Required arguments missing" - echo - echo "Usage: $PROGRAM_NAME secret generate SECRET VERSION [PW]" - exit fi PW=$($PWGEN) @@ -338,7 +381,7 @@ sub_secret_generate(){ ###### Subcommand `run` sub_run(){ - require_stack + load_instance if [ -n "$abra___user" ]; then RUN_USER="-u $abra___user" @@ -358,19 +401,79 @@ sub_run(){ return } -###### Subcommand `deploy` +###### Subcommand `new` + +sub_new (){ + require_abra_dir + + APP=$abra__app_ + SERVER=$abra___server + DOMAIN=$abra___domain + + if [ -z "$SERVER" ]; then + shopt -s nullglob dotglob + SERVERS=(~/.abra/servers/*) + shopt -u nullglob dotglob + + echo "Where would you like to put $APP?" + + echo -n "$(tput setaf 4)" + for i in "${!SERVERS[@]}"; do + printf " %s)\t%s\n" "$i" "${SERVERS[$i]}" + done + echo -n "$(tput sgr0)" + + read -rp "Choose server 0-$i, or Ctrl+C to quit: " SERVER_CHOICE + + SERVER="${SERVERS[$SERVER_CHOICE]}" + else + SERVER="$ABRA_DIR/servers/$SERVER" + fi + + if [ ! -d "$SERVER" ]; then + error "Server '$SERVER' not found" + fi + + APP_DIR="$ABRA_DIR/apps/$APP" + + if [ ! -d "$APP_DIR" ]; then + # FIXME 3wc: move to require_app_clone or something + warning "'$APP' not found, fetching via git.." + + git clone "$GIT_URL/$APP.git" "$ABRA_DIR/apps/$APP" + fi + + if [ -z "$DOMAIN" ]; then + read -rp "Domain name: " DOMAIN + fi + + ENV_FILE="$SERVER/$DOMAIN.env" + + if [ -f "$ENV_FILE" ]; then + error "$ENV_FILE already exists" + # FIXME 3wc: offer to user $STACK_$DOMAIN.env name instead + fi + + cp "$APP_DIR/.envrc.sample" "$ENV_FILE" + sed -i "s/$APP\.example\.com/$DOMAIN/g" "$ENV_FILE" + + echo "$(tput setaf 4)Your new '$APP' is ready for action:$(tput sgr0)" + echo " $(tput setaf 3)Customise the configuration:" + echo " $ENV_FILE$(tput sgr0)" + echo " $(tput setaf 2)Deploy it:" + echo " abra deploy $DOMAIN$(tput sgr0)" +} sub_deploy (){ - require_stack - require_stack_dir - load_context + load_instance + load_instance_env echo "About to deploy:" - echo " Context: $(tput setaf 4)${DOCKER_CONTEXT}$(tput sgr0)" + echo " Server: $(tput setaf 4)${SERVER}$(tput sgr0)" if [ "${COMPOSE_FILE/:/}" == "${COMPOSE_FILE}" ]; then - echo " Compose: $(tput setaf 3)${ABRA_STACK_DIR}/${COMPOSE_FILE}$(tput sgr0)" + echo " Compose: $(tput setaf 3)${APP_DIR}/${COMPOSE_FILE}$(tput sgr0)" else - echo " Compose: $(tput setaf 3)${ABRA_STACK_DIR}/" + echo " Compose: $(tput setaf 3)${APP_DIR}/" IFS=':' read -ra COMPOSE_FILES <<< "$COMPOSE_FILE" for COMPOSE in "${COMPOSE_FILES[@]}"; do echo " - ${COMPOSE}" @@ -391,7 +494,7 @@ sub_deploy (){ esac ( - cd "$ABRA_STACK_DIR" || error "\$ABRA_STACK_DIR '$ABRA_STACK_DIR' not found" + cd "$APP_DIR" || error "\$APP_DIR '$APP_DIR' not found" # shellcheck disable=SC2086 if docker stack deploy -c ${COMPOSE_FILE/:/ -c } "$STACK_NAME"; then if [ -n "$DOMAIN" ]; then @@ -409,9 +512,11 @@ sub_deploy (){ # Inspired by https://github.com/moby/moby/issues/31458#issuecomment-475411564 sub_multilogs() { - require_stack require_multitail + load_instance + load_instance_env + # Get a list of the service names SERVICES=$(docker stack services --format "{{.Name}}" "${STACK_NAME}") # Sort the service names @@ -426,7 +531,8 @@ sub_multilogs() { } sub_logs (){ - require_stack + load_instance + load_instance_env SERVICE="${abra__service_}" @@ -552,7 +658,8 @@ abra() { # the place to handle the dynamically-defined vars declare abra___stack abra___env abra___config abra__command_ abra__args_ \ abra__secret_ abra__version_ abra__data_ abra___user abra__host_ \ - abra__app_ abra__port_ abra__user_ abra__service_ abra__src_ abra__dst_ + abra__app_ abra__port_ abra__user_ abra__service_ abra__src_ abra__dst_ \ + abra__domain_ abra___server abra___domain DOCOPT_PREFIX=abra_ eval "$(docopt "$@")" @@ -577,12 +684,16 @@ abra() { fi done abra__command_=$(IFS="_"; echo "${PARTS[*]:1}") - break done # Fallback for non-docopt-defined options - # shellcheck disable=SC2086 - "sub_${abra__command_}" ${abra__args_[*]} + CMD="sub_${abra__command_}" + if type "$CMD" > /dev/null 2>&1; then + # shellcheck disable=SC2086 + "$CMD" ${abra__args_[*]} + else + docopt_exit + fi } abra "$@"