The catalogue publishes 51 compose files with the bare form 'ports: [80]',
which binds a random host port on 0.0.0.0: every service answers the internet.
ufw-docker existed to take that back afterwards, as root, on linux only,
because docker writes its own firewall rules and ufw never sees those ports.
Publishing where you mean to solves it at the source. Verified against the
daemon: '- 80' gives 0.0.0.0:32768, '127.0.0.1::80' gives 127.0.0.1:32769.
Same on macOS and on linux, no privilege, and visible in docker ps.
A stack binds with ${MYOS_BIND_PRIVATE|PUBLIC|MESH} and declares what it
means with <PREFIX>_SERVICE[_<port>]_EXPOSE. myos expose reads the resolved
compose configuration and reports what would be opened; --strict fails when a
port faces the world without declaring it, which is what an agent runs against
a server it did not set up.
304 lines
11 KiB
Bash
Executable File
304 lines
11 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 expose; 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|export) 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_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)
|
|
-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 status logs config exec run inspect and enter them
|
|
ls [--groups] list the stacks myos can see
|
|
env [VAR...] show resolved variables
|
|
export every setting of the stacks, as KEY=value
|
|
env-update fill .env from the .env.dist templates
|
|
expose [--strict] what the stacks publish, and to whom
|
|
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 ;;
|
|
# running on remote hosts is not implemented yet; the flag is refused
|
|
# rather than silently ignored
|
|
-H) myos_die "$MYOS_E_USAGE" "-H is not implemented yet: run myos on the host itself" ;;
|
|
-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|export|expose|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"
|