lib/str.sh gains firstword, lastword, or, patsubst, filter, filter_out, addprefix, addsuffix and jwt: what the catalogue .mk files are written in. The make JWT macro split on the commas of its payload; this one does not. lib/context.sh registers COMPOSE_PROJECT_NAME, APP, DOCKER_NETWORK_* and the rest as lazy defaults, so a converted .mk keeps reading them as it did. Hooks also load a directory-level _stack.env and _stack.sh, for the .mk files of the catalogue that hold settings for several stacks at once.
302 lines
10 KiB
Bash
Executable File
302 lines
10 KiB
Bash
Executable File
#!/bin/sh
|
|
# myos - Make Your Own Stack
|
|
#
|
|
# shellcheck disable=SC2034 # most globals here are read by lib/ and lib/cmd/
|
|
# shellcheck disable=SC1091 # lib files are sourced by path at runtime
|
|
#
|
|
# Runs docker compose stacks: on a host, in a project directory, for a user.
|
|
# See README.md, or `myos help`.
|
|
set -u
|
|
|
|
MYOS_VERSION=2.0.0-dev
|
|
|
|
# --- locate the installation ---------------------------------------------
|
|
_self=$0
|
|
while [ -L "$_self" ]; do
|
|
_link=$(readlink "$_self")
|
|
case $_link in /*) _self=$_link ;; *) _self=$(dirname "$_self")/$_link ;; esac
|
|
done
|
|
MYOS_ROOT=$(cd "$(dirname "$_self")/.." && pwd -P)
|
|
export MYOS_ROOT
|
|
|
|
for _m in core str var tags naming stack config compose hooks context; do
|
|
# shellcheck source=/dev/null
|
|
. "$MYOS_ROOT/lib/$_m.sh"
|
|
done
|
|
|
|
# myos_is_command WORD true when WORD names a command rather than a stack
|
|
myos_is_command() {
|
|
case $1 in
|
|
up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) return 0 ;;
|
|
version|help) return 0 ;;
|
|
print-*|stack-*-*) return 0 ;;
|
|
*@*) myos_is_command "${1%@*}"; return $? ;;
|
|
esac
|
|
[ -f "$MYOS_ROOT/lib/cmd/$1.sh" ]
|
|
}
|
|
|
|
# --- command line ---------------------------------------------------------
|
|
# These are read by the lib/cmd/* files sourced further down.
|
|
# shellcheck disable=SC2034
|
|
{
|
|
MYOS_CMDS=
|
|
MYOS_REFS=
|
|
MYOS_REFS_RAW=
|
|
MYOS_VARS=
|
|
MYOS_ARGS=
|
|
MYOS_HOSTS=
|
|
MYOS_COLOR=${MYOS_COLOR:-auto}
|
|
VERBOSE=${VERBOSE:-}
|
|
DEBUG=${DEBUG:-}
|
|
}
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...]
|
|
|
|
Options:
|
|
-C DIR work in DIR instead of the current directory
|
|
-e ENV environment (default: local, or ENV from the config)
|
|
-H HOSTS run on remote hosts instead (comma separated, or "all")
|
|
-n, --dry-run print the commands instead of running them
|
|
--color WHEN always, never or auto (default: colour when on a terminal)
|
|
-v, --verbose show what myos does
|
|
-d, --debug show every command
|
|
-h, --help this help
|
|
|
|
Commands:
|
|
up down start stop restart recreate manage the containers of a stack
|
|
ps logs config exec run inspect and enter them
|
|
ls [--groups] list the stacks myos can see
|
|
env [VAR...] show resolved variables
|
|
env-update fill .env from the .env.dist templates
|
|
doctor check the installation
|
|
version print the myos version
|
|
|
|
Stacks:
|
|
myos up the stack of the current directory
|
|
myos up host a group, expanded from host=... in a .env or .mk
|
|
myos up host/fabio a single stack
|
|
myos up postgres:9.6 a versioned stack
|
|
USAGE
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
-C) WORKDIR=$2; shift 2 ;;
|
|
-e) ENV=$2; shift 2 ;;
|
|
-H) MYOS_HOSTS=$2; shift 2 ;;
|
|
-n|--dry-run) DRYRUN=true; shift ;;
|
|
--color) MYOS_COLOR=$2; shift 2 ;;
|
|
--color=*) MYOS_COLOR=${1#--color=}; shift ;;
|
|
-v|--verbose) VERBOSE=true; shift ;;
|
|
-d|--debug) DEBUG=true; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; MYOS_ARGS="$*"; break ;;
|
|
-*)
|
|
if [ -n "$MYOS_CMDS" ]; then
|
|
MYOS_ARGS="${MYOS_ARGS:+$MYOS_ARGS }$1"; shift
|
|
else
|
|
myos_error "unknown option: $1"; usage >&2; exit "$MYOS_E_USAGE"
|
|
fi ;;
|
|
*=*) eval "${1%%=*}=\${1#*=}"; export "${1%%=*}"; shift ;;
|
|
*)
|
|
# Leading words that name commands are commands, the rest are stacks:
|
|
# `myos build up logs host/fabio` runs three commands on one stack, the
|
|
# way `make build up logs STACK=host/fabio` did.
|
|
if [ -z "$MYOS_REFS" ] && myos_is_command "$1"; then
|
|
MYOS_CMDS="${MYOS_CMDS:+$MYOS_CMDS }$1"
|
|
else
|
|
MYOS_REFS="${MYOS_REFS:+$MYOS_REFS }$1"
|
|
fi
|
|
shift ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$MYOS_CMDS" ]; then
|
|
if [ -n "$MYOS_REFS" ]; then
|
|
# the first word was meant as a command; name it rather than dump the usage
|
|
myos_error "unknown command: ${MYOS_REFS%% *}"
|
|
myos_error "to act on a stack of that name, say what to do: myos up ${MYOS_REFS%% *}"
|
|
exit "$MYOS_E_USAGE"
|
|
fi
|
|
usage
|
|
exit "$MYOS_E_USAGE"
|
|
fi
|
|
MYOS_REFS_RAW=$MYOS_REFS
|
|
|
|
# Targets of the make engine keep working: print-VAR, stack-<stack>-<command>
|
|
# and <command>@<env> each translate to a command of the CLI.
|
|
_cmds=
|
|
for _c in $MYOS_CMDS; do
|
|
case $_c in
|
|
*@*) ENV=${_c#*@}; _c=${_c%@*} ;;
|
|
esac
|
|
case $_c in
|
|
print-*)
|
|
MYOS_VARS="${MYOS_VARS:+$MYOS_VARS }${_c#print-}"
|
|
# shellcheck disable=SC2209 # the literal string "env", not the command
|
|
_c=env ;;
|
|
stack-*-*)
|
|
_rest=${_c#stack-}
|
|
_c=${_rest##*-}
|
|
MYOS_REFS="${_rest%-*}${MYOS_REFS:+ $MYOS_REFS}" ;;
|
|
esac
|
|
_cmds="${_cmds:+$_cmds }$_c"
|
|
done
|
|
MYOS_CMDS=$_cmds
|
|
|
|
# env, ls and doctor take variable names where the others take stacks
|
|
case $MYOS_CMDS in
|
|
env|ls|doctor)
|
|
[ -n "$MYOS_VARS" ] || MYOS_VARS=$MYOS_REFS
|
|
MYOS_REFS= ;;
|
|
esac
|
|
|
|
# --- configuration --------------------------------------------------------
|
|
WORKDIR=${WORKDIR:-$PWD}
|
|
WORKDIR=$(cd "$WORKDIR" 2>/dev/null && pwd -P) || myos_die "$MYOS_E_USAGE" "no such directory: $WORKDIR"
|
|
|
|
# ENV decides which .env.<env> to read, so it is resolved first, from the most
|
|
# specific source that names it.
|
|
if [ -z "${ENV:-}" ]; then
|
|
for _f in "$WORKDIR/.env" "${HOME:-}/.config/myos/config" $(myos_conf_files); do
|
|
ENV=$(myos_dotenv_parse "$_f" | sed -n 's/^ENV=//p' | tail -1)
|
|
[ -n "$ENV" ] && break
|
|
done
|
|
fi
|
|
ENV=${ENV:-local}
|
|
|
|
# The layers, most specific first: the loader never overwrites a value, so the
|
|
# order below is the order of precedence. The environment and the VAR=value
|
|
# arguments are already set, and therefore win over every file.
|
|
# MYOS_CONF_PRIORITY=system puts the machine files first, as the make engine did.
|
|
myos_config_layers() {
|
|
if [ "${MYOS_CONF_PRIORITY:-}" = system ]; then
|
|
myos_conf_files
|
|
printf '%s\n' "${HOME:-}/.config/myos/config" "$WORKDIR/.env.$ENV" "$WORKDIR/.env"
|
|
else
|
|
printf '%s\n' "$WORKDIR/.env.$ENV" "$WORKDIR/.env" "${HOME:-}/.config/myos/config"
|
|
myos_conf_files
|
|
fi
|
|
}
|
|
for _f in $(myos_config_layers); do myos_dotenv_load "$_f"; done
|
|
|
|
# Overlay switches. Their names drive which <stack>.<suffix>.yml files load,
|
|
# so these defaults decide that e.g. supabase.labels.yml is picked up.
|
|
COMPOSE_FILE_APP=${COMPOSE_FILE_APP:-true}
|
|
COMPOSE_FILE_LABELS=${COMPOSE_FILE_LABELS:-true}
|
|
COMPOSE_FILE_NETWORKS=${COMPOSE_FILE_NETWORKS:-true}
|
|
COMPOSE_FILE_SSH=${COMPOSE_FILE_SSH:-true}
|
|
COMPOSE_FILE_VOLUMES=${COMPOSE_FILE_VOLUMES:-true}
|
|
COMPOSE_FILE_DEBUG=${COMPOSE_FILE_DEBUG:-${DEBUG:+true}}
|
|
|
|
USER=${USER:-$(id -nu 2>/dev/null)}
|
|
HOSTNAME=${HOSTNAME:-$(hostname 2>/dev/null | sed 's/\..*//')}
|
|
HOSTNAME=$(myos_lower "$HOSTNAME")
|
|
DOMAIN=${DOMAIN:-localhost}
|
|
DOMAINNAME=${DOMAINNAME:-${DOMAIN%% *}}
|
|
MAIL=${MAIL:-$(git config user.email 2>/dev/null || printf '%s@%s' "$USER" "$DOMAINNAME")}
|
|
DRYRUN=${DRYRUN:-false}
|
|
myos_colors
|
|
|
|
# --- stack references -----------------------------------------------------
|
|
# No reference given: the configured STACK, else the current directory when it
|
|
# holds a compose file. An explicit STACK always wins, so a project that pins
|
|
# its stacks in .env keeps working from inside its own directory.
|
|
if [ -z "$MYOS_REFS" ]; then
|
|
if [ -n "${STACK:-}" ]; then
|
|
MYOS_REFS=$STACK
|
|
elif [ -f "$WORKDIR/docker-compose.yml" ] || [ -f "$WORKDIR/compose.yml" ] ||
|
|
[ -f "$WORKDIR/docker/docker-compose.yml" ]; then
|
|
MYOS_REFS=./
|
|
fi
|
|
fi
|
|
if [ -z "$MYOS_REFS" ]; then
|
|
# these commands describe the installation rather than act on a stack
|
|
for _c in $MYOS_CMDS; do
|
|
case $_c in
|
|
env|env-update|ls|doctor|version|help) ;;
|
|
*) myos_die "$MYOS_E_USAGE" "no stack given, and no compose file in $WORKDIR" ;;
|
|
esac
|
|
done
|
|
fi
|
|
|
|
# shellcheck disable=SC2086 # a list of references
|
|
MYOS_STACKS=$(myos_group_expand $MYOS_REFS)
|
|
|
|
myos_context_defaults
|
|
|
|
# The uri a stack is served on: the tag helpers build on it, so it has to be
|
|
# known before the hooks run.
|
|
MYOS_SCOPE_FIRST=$(for _r in $MYOS_STACKS; do myos_scope "$_r"; break; done)
|
|
MYOS_APP_FIRST=$(for _r in $MYOS_STACKS; do myos_stack_name "$_r"; break; done)
|
|
APP_HOST=${APP_HOST:-$(myos_app_host "$MYOS_SCOPE_FIRST" "$USER" "$ENV" "$MYOS_APP_FIRST" "$DOMAINNAME" "$HOSTNAME")}
|
|
APP_URI=${APP_URI:-$(myos_app_uri "$APP_HOST" "${APP_PATH:-}")}
|
|
APP_SCHEME=${APP_SCHEME:-http}
|
|
|
|
# Per-stack hooks must run in this shell: everything downstream reads the
|
|
# variables they set, and a command substitution would throw them away.
|
|
for _ref in $MYOS_STACKS; do
|
|
for _hdir in $(myos_stack_dirs "$_ref"); do
|
|
myos_stack_hooks "$_hdir" "$(myos_stack_name "$_ref")"
|
|
done
|
|
done
|
|
|
|
# The two functions below are called from the lib/cmd/* files sourced later,
|
|
# which shellcheck cannot see.
|
|
# shellcheck disable=SC2329
|
|
# myos_framework_compose_files the networks and volumes overlays myos itself
|
|
# provides; they always come last so a stack can rely on them being there.
|
|
myos_framework_compose_files() {
|
|
myos_compose_files "$MYOS_ROOT/share/compose" "networks volumes" "$(myos_compose_suffixes)" "$ENV"
|
|
}
|
|
|
|
# shellcheck disable=SC2329
|
|
# myos_stack_compose_files REF the ordered compose files of one reference
|
|
myos_stack_compose_files() {
|
|
_dirs=$(myos_stack_dirs "$1")
|
|
[ -n "$_dirs" ] || { myos_stack_resolve "$1" >/dev/null; return $?; }
|
|
_name=$(myos_stack_name "$1")
|
|
_suffixes="$(myos_compose_suffixes) $(myos_stack_version "$1")"
|
|
for _dir in $_dirs; do
|
|
case $1 in
|
|
.|./*|/*|../*)
|
|
myos_compose_files "$_dir" "docker-compose compose" "$_suffixes" "$ENV"
|
|
myos_compose_files "$_dir/docker" "docker-compose compose" "$_suffixes" "$ENV" ;;
|
|
*)
|
|
myos_compose_files "$_dir" "docker-compose $_name" "$_suffixes" "$ENV" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# --- dispatch -------------------------------------------------------------
|
|
# myos_dispatch COMMAND run one command
|
|
myos_dispatch() {
|
|
case $1 in
|
|
version) printf 'myos %s\n' "$MYOS_VERSION"; return 0 ;;
|
|
help) usage; return 0 ;;
|
|
esac
|
|
if [ -f "$MYOS_ROOT/lib/cmd/$1.sh" ]; then
|
|
# shellcheck source=/dev/null
|
|
. "$MYOS_ROOT/lib/cmd/$1.sh"
|
|
"myos_cmd_$(printf '%s' "$1" | tr '-' '_')"
|
|
return $?
|
|
fi
|
|
# commands that map straight onto docker compose
|
|
case $1 in
|
|
up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) ;;
|
|
*) myos_error "unknown command: $1"; usage >&2; return "$MYOS_E_USAGE" ;;
|
|
esac
|
|
# shellcheck source=/dev/null
|
|
. "$MYOS_ROOT/lib/cmd/_compose.sh"
|
|
myos_cmd_compose "$1"
|
|
}
|
|
|
|
# Several commands run in order and stop at the first failure, as make did.
|
|
_rc=0
|
|
for MYOS_CMD in $MYOS_CMDS; do
|
|
myos_dispatch "$MYOS_CMD" || { _rc=$?; break; }
|
|
done
|
|
exit "$_rc"
|