diff --git a/ceru/ceru b/ceru/ceru new file mode 100755 index 0000000..bc4a4b1 --- /dev/null +++ b/ceru/ceru @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ======== CONFIGURATION ======== +CERU_USE_NIX3=true +# ======== CONFIGURATION ======== + +# libceru env vars +THIS=$(basename "$0") +source libceru.sh + +USAGE="${BOLD}${UNDERLINE}${RED}Usage${RESET} + ${BOLD}${GREEN}$THIS [option...] subcommand${RESET} + +${BOLD}${UNDERLINE}${RED}Options${RESET} + ${BOLD}${MAGENTA}-h, --help${RESET} Show this message (^_^) + +${BOLD}${UNDERLINE}${RED}Subcommands${RESET} + ${BOLD}${CYAN}deploy${RESET} Deploy your Cerulean network + ${BOLD}${CYAN}new${RESET} Init new instances of various components (see ${BOLD}${GREEN}\`$THIS new --help\`${RESET})" + +# parse all args +SUBCMD=false # whether a subcommand was specified +while [[ $# -gt 0 ]]; do + ARG=$1 + case $ARG in + -h|--help) + throw-usage 0 ;; + -*) + echo "[!] Unknown option \"$ARG\"" + exit 1 ;; + *) + SUBCMD=true + break ;; + esac +done; unset -v ARG + +# invalid usage occurs if no args or subcommand given +if [[ $# = 0 || "$SUBCMD" = false ]]; then + throw-usage 1 +fi; unset -v SUBCMD + +# run provided subcommand +run-subcmd "$@" diff --git a/ceru/libceru.sh b/ceru/libceru.sh new file mode 100755 index 0000000..92d21d7 --- /dev/null +++ b/ceru/libceru.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +# libceru relies on the script that sources +# it to set the following environment variables: +# CMD_NAME USAGE +set -u + +# ======== INTERNAL STATE ======== +SUBCMDS="$PWD/subcmds" # XXX: TODO: don't hardcode as relative +CMD_ABS="$SUBCMDS" # (initial value) CMD_ABS stores the current cmd's absolute path +CMD_MAJ="$THIS" # (initial value) CMD_MAJ stores the current cmd's parent's name +CMD_MIN="" # (initial value) CMD_MIN stores the current cmd's name +# ======== INTERNAL STATE ======== + +# ANSI Coloring +BLACK='\033[30m' +RED='\033[31m' +GREEN='\033[32m' +YELLOW='\033[33m' +BLUE='\033[34m' +MAGENTA='\033[35m' +CYAN='\033[36m' +WHITE='\033[37m' +DEFCOL='\033[39m' # default colour + +# ANSI Styling +RESET='\033[0m' +BOLD='\033[1m' +DIM='\033[2m' +ITALIC='\033[3m' +UNDERLINE='\033[4m' +BLINKSLOW='\033[5m' +BLINKFAST='\033[6m' +REVERSE='\033[7m' +INVISIBLE='\033[8m' + +# Error Messages +function perr { echo -e "${BOLD}${RED}error:${RESET} $@\nTry ${BOLD}${GREEN}'--help'${RESET} for more information." >&2; } +function perr-usage { echo -e "$USAGE" >&2; } +function perr-badflag { perr "unrecognised flag ${BOLD}${MAGENTA}'$1'${RESET}"; } +function perr-noflagval { perr "flag ${BOLD}${MAGENTA}'$1'${RESET} requires ${BOLD}${MAGENTA}${2}${RESET} argument(s), but only ${BOLD}${MAGENTA}${3}${RESET} were given"; } +function perr-badarg { perr "unrecognised arg ${BOLD}${MAGENTA}'$1'${RESET}"; } +function perr-noarg { perr "required argument ${BOLD}${MAGENTA}'$1'${RESET} is missing"; } +# Failures +function throw { echo -e "${@:2}" >&2; if [[ "$1" -ge 0 ]]; then exit "$1"; fi; } +function throw-usage { throw "$1" "$(perr-usage 2>&1)"; } +function throw-badflag { throw "$1" "$(perr-badflag "${@:2}" 2>&1)"; } +function throw-noflagval { throw "$1" "$(perr-noflagval "${@:2}" 2>&1)"; } +function throw-badarg { throw "$1" "$(perr-badarg "${@:2}" 2>&1)"; } +function throw-noarg { throw "$1" "$(perr-noarg "${@:2}" 2>&1)"; } +# Parsing/Validation +function required { [[ -n "$1" ]] || throw-noarg 1 "${@:2}"; } + +# Other +function confirm-action { + local CHAR + while :; do + echo -e "$1" + read -n1 CHAR + case $CHAR in + [yY]) + return 0 ;; + [nN]) + return 1 ;; + esac + done +} +function confirm { confirm-action ":: Proceed? [Y/n] "; } + +function confirm-file-overwrite { + echo -e "${BOLD}${UNDERLINE}${BLINKFAST}${RED}WARNING!${RESET} ${YELLOW}The following files will be overwritten:${RESET}" + local ARG="" + for ARG in "$@"; do + echo -e "${BOLD} • ${GREEN}${ARG}${RESET}" + done + confirm +} + +# ====== Core ====== +function run-subcmd { + # if CMD_MIN is empty, then CMD_MAJ is the root cmd (ie ceru) + # and hence CMD_MAJ shouldn't yet be overridden + if [[ ! "$CMD_MIN" = "" ]]; then + CMD_MAJ="$CMD_MIN" # swapsies! + fi + # we shift here so $@ is passed correctly when we `source "$TARGET"` + CMD_MIN="$1"; shift + + # ensure the current command can take subcommands + if [[ -f "$CMD_ABS" ]]; then + # XXX: INTERNAL ERROR + throw 2 "Subcommand \"$CMD_MIN\" cannot exist, as $CMD_MAJ has no subcommands!" + fi + + CMD_ABS="$CMD_ABS/$CMD_MIN" + + # attempt to find the script corresponding to CMD_ABS + TARGET="$CMD_ABS" + if [[ -d "$CMD_ABS" ]]; then + TARGET="$TARGET/default.sh" + elif [[ ! -f "$CMD_ABS" ]]; then + throw 1 "Command \"$CMD_MAJ\" does not provide subcommand \"$CMD_MIN\"!" + fi + source "$TARGET" +} diff --git a/ceru/subcmds/new/default.sh b/ceru/subcmds/new/default.sh new file mode 100755 index 0000000..8cfce8f --- /dev/null +++ b/ceru/subcmds/new/default.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail +USAGE="${BOLD}${UNDERLINE}${RED}Usage${RESET} + ${BOLD}${GREEN}$THIS new [option...] subcommand${RESET} + +${BOLD}${UNDERLINE}${RED}Options${RESET} + ${BOLD}${MAGENTA}-h, --help${RESET} Show this message (^_^) + +${BOLD}${UNDERLINE}${RED}Subcommands${RESET} + ${BOLD}${CYAN}key${RESET} Generate a new binary-cache signing keypair" + +# parse all args +SUBCMD=false # where a subcommand was specified +while [[ $# -gt 0 ]]; do + ARG=$1 + case $ARG in + -h|--help) + throw-usage 0 ;; + -*) + echo "[!] Unknown option \"$ARG\"" + exit 1 ;; + *) + SUBCMD=true + break ;; + esac +done; unset -v ARG + +# invalid usage occurs if no args or subcommand given +if [[ $# = 0 || "$SUBCMD" = false ]]; then + throw-usage 1 +fi; unset -v SUBCMD + +# run provided subcommand +run-subcmd "$@" diff --git a/ceru/subcmds/new/key b/ceru/subcmds/new/key new file mode 100755 index 0000000..41b4916 --- /dev/null +++ b/ceru/subcmds/new/key @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail +USAGE="${BOLD}${UNDERLINE}${RED}Usage${RESET} + ${BOLD}${GREEN}$THIS new key [option...]${RESET} + +${BOLD}${UNDERLINE}${RED}Options${RESET} + ${BOLD}${MAGENTA}-h, --help${RESET} Show this message (^_^) + ${BOLD}${MAGENTA}-f, --force${RESET} Ignores all warnings!! + ${BOLD}${MAGENTA}-j, --json${RESET} Output in JSON format + ${BOLD}${MAGENTA}-n, --name${RESET} Identifier of the key (e.g. ${BOLD}${MAGENTA}cache.example.org-1${RESET}) + ${BOLD}${MAGENTA}-o, --out${RESET} Private key file name to write to (the public key is named identically but ends with ${BOLD}${MAGENTA}.pub${RESET}) + ${BOLD}${MAGENTA}-P, --private-only${RESET} Only generate the private key, not the entire keypair" + +# ==== Argument Values ==== +FORCE=false +PRIV_ONLY=false +JSON=false +NAME="" +SINK="" +# ==== Argument Values ==== + +# parse all args +while [[ $# -gt 0 ]]; do + ARG="$1" + case "$ARG" in + -h|--help) + throw-usage 0 + ;; + -n|--name) + shift + # XXX: NOTE: do I need to safe shift (shift || true) since -e is set? + NAME="$1"; shift + ;; + -P|--private-only) + shift + PRIV_ONLY=true + ;; + -o|--out) + shift + # XXX: NOTE: do I need to safe shift (shift || true) since -e is set? + SINK="$1"; shift + ;; + -j|--json) + shift + JSON=true + ;; + -f|--force) + shift + FORCE=true + ;; + + -*) + throw-badflag 1 "$ARG" + ;; + *) + throw-badarg 1 "$ARG" + ;; + esac +done; unset -v ARG + +# fail if NAME not provided +required "$NAME" --name + + +# generate our keypair +PRIV_KEY=$(nix key generate-secret --key-name "$NAME") +PUB_KEY=$(nix key convert-secret-to-public <<<"$PRIV_KEY") + +# result defaults to unset (only stays unset if we intend on writing to a file) +RESULT="" +# JSON formatting +if [[ "$JSON" = true ]]; then + RESULT="{ + \"privateKey\": \"${PRIV_KEY}\"$( + [[ "$PRIV_ONLY" = true ]] \ + || echo ",\n \"publicKey\": \"${PUB_KEY}\"") +}" + if [[ -n "$SINK" ]]; then + # confirm the user understands files will be overwritten + [[ "$FORCE" = true ]] || confirm-file-overwrite "$SINK" || exit 0 + echo -e "$RESULT" > "$SINK" + else + echo -e "$RESULT" + fi +# standard formatting (stdout) +elif [[ -z "$SINK" ]]; then + echo -e "${BOLD}${UNDERLINE}${RED}Private Key${RESET} + ${BOLD}${GREEN}${PRIV_KEY}${RESET}$( + [[ "$PRIV_ONLY" = true ]] \ + || echo "\n${BOLD}${UNDERLINE}${RED}Public Key${RESET}\n ${BOLD}${GREEN}${PUB_KEY}${RESET}")" +# standard formatting (files) +else + PRIV_SINK="$SINK" + PUB_SINK="$SINK.pub" + # confirm the user understands files will be overwritten + [[ "$FORCE" = true ]] || confirm-file-overwrite "$PRIV_SINK" "$PUB_SINK" || exit 0 + echo "$PRIV_KEY" > "$PRIV_SINK" + echo "$PUB_KEY" > "$PUB_SINK" +fi; +unset -v PRIV_SINK PUB_SINK PRIV_KEY PUB_KEY RESULT