Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90bb97cca8 | ||
|
|
60668fc80a | ||
|
|
f2bcbc6857 | ||
|
|
ca7338bcb6 |
@@ -20,6 +20,11 @@
|
||||
- the project `.env` now wins over `/etc/conf.d/myos`, as documented;
|
||||
`MYOS_CONF_PRIORITY=system` restores the previous order
|
||||
- `share/make/shim.mk`: make as an optional front end over the same shell code
|
||||
- `myos expose` reports what each stack publishes and to whom, and `--strict`
|
||||
fails when a port faces the world without saying so. `MYOS_BIND_PUBLIC`,
|
||||
`_PRIVATE` and `_MESH` let a stack bind its published ports, which replaces
|
||||
the linux-only ufw-docker patching with something that behaves the same on
|
||||
macOS and needs no privilege
|
||||
- commands chain: `myos build up logs host/fabio`, as make targets did
|
||||
- the stack catalogue no longer needs make at all: its settings are hooks, and
|
||||
only six stacks keep a .mk, for targets
|
||||
|
||||
@@ -19,7 +19,7 @@ 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
|
||||
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
|
||||
@@ -28,7 +28,7 @@ done
|
||||
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 ;;
|
||||
version|help|export) return 0 ;;
|
||||
print-*|stack-*-*) return 0 ;;
|
||||
*@*) myos_is_command "${1%@*}"; return $? ;;
|
||||
esac
|
||||
@@ -44,7 +44,6 @@ myos_is_command() {
|
||||
MYOS_REFS_RAW=
|
||||
MYOS_VARS=
|
||||
MYOS_ARGS=
|
||||
MYOS_HOSTS=
|
||||
MYOS_COLOR=${MYOS_COLOR:-auto}
|
||||
VERBOSE=${VERBOSE:-}
|
||||
DEBUG=${DEBUG:-}
|
||||
@@ -57,7 +56,6 @@ 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
|
||||
@@ -66,10 +64,12 @@ Options:
|
||||
|
||||
Commands:
|
||||
up down start stop restart recreate manage the containers of a stack
|
||||
ps logs config exec run inspect and enter them
|
||||
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
|
||||
|
||||
@@ -85,7 +85,9 @@ while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
-C) WORKDIR=$2; shift 2 ;;
|
||||
-e) ENV=$2; shift 2 ;;
|
||||
-H) MYOS_HOSTS=$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 ;;
|
||||
@@ -216,7 +218,7 @@ 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) ;;
|
||||
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
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#shellcheck shell=sh
|
||||
# myos export print every setting the requested stacks declare, as KEY=value.
|
||||
#
|
||||
# One call, so a Makefile can read the whole set at once:
|
||||
# $(eval $(shell myos export STACK=host))
|
||||
# Asking for each variable separately costs a process per variable.
|
||||
myos_cmd_export() {
|
||||
# shellcheck disable=SC2209 # these are literal format names, not commands
|
||||
_fmt=sh
|
||||
# shellcheck disable=SC2209
|
||||
case ${MYOS_ARGS:-}${MYOS_VARS:-} in *--make*) _fmt=make ;; esac
|
||||
for _v in $(myos_declared_defaults); do
|
||||
_val=$(myos_var "$_v")
|
||||
case $_fmt in
|
||||
make)
|
||||
# make would expand a $ and start a comment at a #, and := stops it
|
||||
# from expanding the value again later
|
||||
printf '%s := %s\n' "$_v" "$(printf '%s' "$_val" | sed -e 's/\$/$$/g' -e 's/#/\\#/g')" ;;
|
||||
*) printf '%s=%s\n' "$_v" "$_val" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# myos_declared_defaults the variables the loaded hooks declare
|
||||
myos_declared_defaults() {
|
||||
for _ref in $MYOS_STACKS; do
|
||||
for _d in $(myos_stack_dirs "$_ref"); do
|
||||
for _f in "$_d"/_stack.sh "$_d/$(myos_stack_name "$_ref").sh"; do
|
||||
[ -f "$_f" ] && sed -n 's/^myos_default_\([A-Za-z_][A-Za-z0-9_]*\)().*/\1/p' "$_f"
|
||||
done
|
||||
for _f in "$_d"/_stack.env "$_d/$(myos_stack_name "$_ref").env"; do
|
||||
[ -f "$_f" ] && myos_dotenv_parse "$_f" | sed 's/=.*//'
|
||||
done
|
||||
done
|
||||
done | sort -u
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#shellcheck shell=sh
|
||||
# myos expose [--strict] what the stacks publish, and to whom
|
||||
#
|
||||
# Reads the resolved compose configuration, so it reports what `myos up` would
|
||||
# open rather than what happens to be running. A port bound to 0.0.0.0 answers
|
||||
# the internet: on linux docker writes its own firewall rules and the host
|
||||
# firewall does not see it. --strict exits 1 when a port is world-bound
|
||||
# without the stack declaring that scope.
|
||||
myos_cmd_expose() {
|
||||
_strict=false
|
||||
case ${MYOS_ARGS:-}${MYOS_VARS:-} in *--strict*) _strict=true ;; esac
|
||||
|
||||
_rows=$(myos_expose_rows)
|
||||
[ -n "$_rows" ] || { printf 'no published port: nothing is reachable from outside the docker network\n'; return 0; }
|
||||
|
||||
printf '%s%-20s %-14s %-22s %-6s %s%s\n' \
|
||||
"$MYOS_C_HIGHLIGHT" STACK SERVICE "PUBLISHED ON" PORT SCOPE "$MYOS_C_RESET"
|
||||
_bad=0
|
||||
_oIFS=$IFS; IFS='
|
||||
'
|
||||
for _row in $_rows; do
|
||||
IFS=$_oIFS
|
||||
_st=${_row%%|*}; _rest=${_row#*|}
|
||||
_sv=${_rest%%|*}; _rest=${_rest#*|}
|
||||
_on=${_rest%%|*}; _rest=${_rest#*|}
|
||||
_pt=${_rest%%|*}; _sc=${_rest#*|}
|
||||
case ${_on%:*} in
|
||||
0.0.0.0|''|'::'|'*')
|
||||
[ "$_sc" = public ] || _bad=$((_bad + 1))
|
||||
printf '%-20s %-14s %s%-22s%s %-6s %s\n' \
|
||||
"$_st" "$_sv" "$MYOS_C_WARN" "$_on" "$MYOS_C_RESET" "$_pt" "$_sc" ;;
|
||||
*)
|
||||
printf '%-20s %-14s %-22s %-6s %s\n' "$_st" "$_sv" "$_on" "$_pt" "$_sc" ;;
|
||||
esac
|
||||
IFS='
|
||||
'
|
||||
done
|
||||
IFS=$_oIFS
|
||||
|
||||
if [ "$_bad" -gt 0 ]; then
|
||||
myos_warning "$_bad port(s) reachable from anywhere without declaring the public scope"
|
||||
myos_warning "bind them: ports: [\"\${MYOS_BIND_PRIVATE}::<port>\"]"
|
||||
[ "$_strict" = true ] && return "$MYOS_E_FAIL"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# myos_expose_rows STACK|SERVICE|ADDR:PORT|CONTAINER_PORT|SCOPE for every
|
||||
# published port of the requested stacks
|
||||
myos_expose_rows() {
|
||||
for _ref in $MYOS_STACKS; do
|
||||
_files=$(myos_stack_compose_files "$_ref" 2>/dev/null) || continue
|
||||
[ -n "$_files" ] || continue
|
||||
_fw=$(myos_framework_compose_files)
|
||||
[ -n "$_fw" ] && _files="$_files
|
||||
$_fw"
|
||||
_app=$(myos_stack_name "$_ref")
|
||||
_project=$(myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$_app")
|
||||
DRYRUN=false myos_compose "$_project" "$_files" -- config 2>/dev/null |
|
||||
myos_expose_parse "$_ref" "$(myos_stack_prefix "$_ref")"
|
||||
done
|
||||
}
|
||||
|
||||
# myos_expose_parse STACK NAME (compose config on stdin)
|
||||
# compose normalises every port to the long form, so one shape is enough
|
||||
myos_expose_parse() {
|
||||
awk -v stack="$1" '
|
||||
/^services:/ { insvc = 1; next }
|
||||
insvc && /^ [a-zA-Z0-9_.-]+:/ { svc = $1; sub(/:$/, "", svc); inports = 0 }
|
||||
insvc && /^ ports:/ { inports = 1; next }
|
||||
inports && /^ [a-z]/ { inports = 0 }
|
||||
inports && /host_ip:/ { ip = $2 }
|
||||
inports && /published:/ { pub = $2; gsub(/"/, "", pub) }
|
||||
inports && /target:/ { tgt = $2 }
|
||||
inports && /protocol:/ {
|
||||
printf "%s|%s|%s:%s|%s\n", stack, svc, (ip == "" ? "0.0.0.0" : ip), pub, tgt
|
||||
ip = ""; pub = ""; tgt = ""
|
||||
}
|
||||
' | while IFS='|' read -r _s _v _o _t; do
|
||||
printf '%s|%s|%s|%s|%s\n' "$_s" "$_v" "$_o" "$_t" "$(myos_expose_scope "$2" "$_v" "$_t")"
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#shellcheck shell=sh
|
||||
# myos recreate remove the containers and create them again
|
||||
# myos reload the same, under the name the make engine used
|
||||
myos_cmd_recreate() {
|
||||
# shellcheck source=lib/cmd/_compose.sh
|
||||
. "$MYOS_ROOT/lib/cmd/_compose.sh"
|
||||
MYOS_ARGS="--force-recreate ${MYOS_ARGS:-}"
|
||||
myos_cmd_compose up
|
||||
}
|
||||
myos_cmd_reload() { myos_cmd_recreate; }
|
||||
@@ -0,0 +1,4 @@
|
||||
#shellcheck shell=sh
|
||||
# myos reload: see lib/cmd/recreate.sh
|
||||
# shellcheck source=lib/cmd/recreate.sh
|
||||
. "$MYOS_ROOT/lib/cmd/recreate.sh"
|
||||
@@ -0,0 +1,7 @@
|
||||
#shellcheck shell=sh
|
||||
# myos status what is running, under the name the make engine used for ps
|
||||
myos_cmd_status() {
|
||||
# shellcheck source=lib/cmd/_compose.sh
|
||||
. "$MYOS_ROOT/lib/cmd/_compose.sh"
|
||||
myos_cmd_compose ps
|
||||
}
|
||||
@@ -58,6 +58,10 @@ myos_context_defaults() {
|
||||
myos_default_HOST() { myos_addprefix "${HOSTNAME:-}." "$(myos_var DOMAIN)"; }
|
||||
myos_default_HOSTNAME() { printf '%s' "${HOSTNAME:-}"; }
|
||||
myos_default_DOMAINNAME() { myos_firstword "$(myos_var DOMAIN)"; }
|
||||
# the addresses a stack binds its published ports to
|
||||
myos_default_MYOS_BIND_PUBLIC() { myos_bind public; }
|
||||
myos_default_MYOS_BIND_PRIVATE() { myos_bind private; }
|
||||
myos_default_MYOS_BIND_MESH() { myos_bind mesh; }
|
||||
myos_default_MACHINE() { uname -m 2>/dev/null; }
|
||||
myos_default_SYSTEM() { uname -s 2>/dev/null; }
|
||||
myos_default_HOST_COMPOSE_PROJECT_NAME() { printf '%s' "${HOSTNAME:-}"; }
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#shellcheck shell=sh
|
||||
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
|
||||
# expose: which addresses a published port binds to.
|
||||
#
|
||||
# Docker writes its own firewall rules, so on linux a port published with
|
||||
# `-p 8080:80` answers the internet whatever the host firewall says. ufw-docker
|
||||
# patches that afterwards, on linux only, as root.
|
||||
#
|
||||
# The portable answer is to publish where you mean to in the first place:
|
||||
# `-p 127.0.0.1:8080:80` only ever listens on the loopback, identically on
|
||||
# linux and on macOS, with no firewall and no privilege. A stack says which
|
||||
# scope a port belongs to, and myos resolves the address.
|
||||
#
|
||||
# public the internet: a load balancer, a public DNS or mail service
|
||||
# mesh the private network between the hosts of the fleet
|
||||
# private this host only: everything the load balancer reaches for you
|
||||
#
|
||||
# MYOS_BIND_<SCOPE> overrides any of them.
|
||||
|
||||
# myos_bind SCOPE the address a port of that scope binds to
|
||||
myos_bind() {
|
||||
case $1 in
|
||||
public) printf '%s' "${MYOS_BIND_PUBLIC:-0.0.0.0}" ;;
|
||||
mesh) printf '%s' "${MYOS_BIND_MESH:-$(myos_bind_mesh)}" ;;
|
||||
private|*) printf '%s' "${MYOS_BIND_PRIVATE:-127.0.0.1}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# myos_bind_mesh the address of the mesh interface, empty when there is none.
|
||||
# Falls back to the private address so that a stack scoped to the mesh on a
|
||||
# host that has none stays local rather than becoming public.
|
||||
myos_bind_mesh() {
|
||||
_if=${MYOS_MESH_IFACE:-}
|
||||
if [ -z "$_if" ]; then
|
||||
for _c in easytier tun0 tailscale0 mycelium wg0; do
|
||||
if myos_iface_addr "$_c" >/dev/null 2>&1 && [ -n "$(myos_iface_addr "$_c")" ]; then
|
||||
_if=$_c; break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[ -n "$_if" ] || { printf '%s' "${MYOS_BIND_PRIVATE:-127.0.0.1}"; return 0; }
|
||||
_a=$(myos_iface_addr "$_if")
|
||||
[ -n "$_a" ] || _a=${MYOS_BIND_PRIVATE:-127.0.0.1}
|
||||
printf '%s' "$_a"
|
||||
}
|
||||
|
||||
# myos_iface_addr NAME the first address of an interface, on linux or macOS
|
||||
myos_iface_addr() {
|
||||
if myos_have ip; then
|
||||
ip -o addr show "$1" 2>/dev/null | awk '$3 ~ /^inet6?$/ {sub(/\/.*/,"",$4); print $4; exit}'
|
||||
elif myos_have ifconfig; then
|
||||
ifconfig "$1" 2>/dev/null | awk '$1 == "inet" || $1 == "inet6" {print $2; exit}'
|
||||
fi
|
||||
}
|
||||
|
||||
# myos_stack_prefix REF the prefix the settings of a stack use.
|
||||
# A host stack is prefixed by HOST_, which is how the catalogue names them:
|
||||
# HOST_FABIO_SERVICE_9998_TAGS, HOST_FTPS_UFW_DOCKER. Everything else uses the
|
||||
# stack name alone: SUPABASE_KONG_SERVICE_8000_TAGS.
|
||||
myos_stack_prefix() {
|
||||
_n=$(myos_upper "$(myos_stack_name "$1")")
|
||||
case $(myos_scope "$1") in
|
||||
host) printf 'HOST_%s' "$_n" ;;
|
||||
user) printf 'USER_%s' "$_n" ;;
|
||||
*) printf '%s' "$_n" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# myos_expose_scope PREFIX SERVICE PORT the scope a stack declares for a port:
|
||||
# <PREFIX>_SERVICE_<port>_EXPOSE, then <PREFIX>_SERVICE_EXPOSE, then the same
|
||||
# two on the service name, else private
|
||||
myos_expose_scope() {
|
||||
_u=$(myos_upper "$1")
|
||||
for _n in "${_u}_SERVICE_${3}_EXPOSE" "${_u}_SERVICE_EXPOSE" \
|
||||
"$(myos_upper "$2")_SERVICE_${3}_EXPOSE" "$(myos_upper "$2")_SERVICE_EXPOSE"; do
|
||||
_s=$(myos_var "$_n")
|
||||
[ -n "$_s" ] && { printf '%s' "$_s"; return 0; }
|
||||
done
|
||||
printf 'private'
|
||||
}
|
||||
+3
-1
@@ -30,7 +30,9 @@ NFS_HOST ?= host.docker.internal
|
||||
SERVICES ?= $(DOCKER_SERVICES)
|
||||
|
||||
envprefix = $(foreach env,$(3),$(if $($(call UPPERCASE,$(1)_SERVICE_$(2)_$(env))),$(env)=$($(call UPPERCASE,$(1)_SERVICE_$(2)_$(env)))))
|
||||
patsublist = $(patsubst $(1),$(2),$(firstword $(3)))$(foreach pattern,$(wordlist 2,255,$(3)),$(comma)$(patsubst $(1),$(2),$(pattern)))
|
||||
## the replacement ends with the options, so an empty option list left a space
|
||||
## before the comma joining two routes: "urlprefix-a/* ,urlprefix-b/*"
|
||||
patsublist = $(subst $(space)$(comma),$(comma),$(patsubst $(1),$(2),$(firstword $(3)))$(foreach pattern,$(wordlist 2,255,$(3)),$(comma)$(patsubst $(1),$(2),$(pattern))))
|
||||
servicenvs = $(foreach env,$(call UPPERCASE,$($(1)_SERVICE_$(2)_ENVS)),$(if $(3),$($(1)_SERVICE_$(env)_$(3)),$($(1)_SERVICE_$(2)_$(env))))
|
||||
tagprefix = $(call urlprefix,$(or $($(call UPPERCASE,$(1)_SERVICE_$(2)_PATH)),$($(call UPPERCASE,$(1)_SERVICE_PATH))),$(or $($(call UPPERCASE,$(1)_SERVICE_$(2)_OPTS)),$($(call UPPERCASE,$(1)_SERVICE_OPTS)),$(call envprefix,$(1),$(2),allow auth deny preprend proto register strip)),$(or $(foreach env,$(3),$($(call UPPERCASE,$(1)_SERVICE_$(2)_$(env)))),$($(call UPPERCASE,$(1)_SERVICE_$(2)_URIS)),$(call uri,$(1),$(2))))
|
||||
uri = $(foreach svc,$(1),$(patsubst %,$(addsuffix .,$(or $($(call UPPERCASE,$(svc)_SERVICE_$(2)_NAME)),$($(call UPPERCASE,$(svc)_SERVICE_NAME)),$(svc)))%,$(or $(3),$(APP_URI))))
|
||||
|
||||
@@ -13,7 +13,9 @@ endif
|
||||
setup-docker-group:
|
||||
ifneq ($(DOCKER),)
|
||||
ifeq ($(or $(filter $(USER),$(subst $(comma), ,$(shell awk -F':' '$$1 == "docker" {print $$4}' /etc/group))),$(filter 0,$(UID))),)
|
||||
$(call ansible-user-add-groups,$(USER),docker)
|
||||
$(RUN) $(SUDO) usermod -aG docker $(USER) 2>/dev/null \
|
||||
|| $(RUN) $(SUDO) addgroup $(USER) docker 2>/dev/null \
|
||||
|| $(call ERROR,unable to add user,$(USER),to group,docker)
|
||||
$(call WARNING,user,$(USER),added in group,docker)
|
||||
endif
|
||||
ifeq ($(filter 0 $(DOCKER_GID),$(GIDS)),)
|
||||
|
||||
+23
-12
@@ -1,10 +1,25 @@
|
||||
##
|
||||
# SSH
|
||||
#
|
||||
# The remote hosts used to come from AWS, through
|
||||
# ssh-get-PrivateIpAddress-% -> aws-ec2-get-instances-PrivateIpAddress-%.
|
||||
# make/apps/aws was removed and AWS_INSTANCE_IP is defined nowhere, so these
|
||||
# targets looped over an empty list and exited 0 without doing anything.
|
||||
# They now take SSH_HOSTS, and say so when it is empty rather than pretending
|
||||
# to have connected.
|
||||
|
||||
# variable SSH_HOSTS: hosts the ssh targets act on, space separated
|
||||
SSH_HOSTS ?= $(AWS_INSTANCE_IP)
|
||||
|
||||
# target ssh-hosts-check: Fail when no remote host is known
|
||||
.PHONY: ssh-hosts-check
|
||||
ssh-hosts-check:
|
||||
$(if $(SSH_HOSTS),,$(call ERROR,no remote host: set SSH_HOSTS=host1 host2))
|
||||
|
||||
# target ssh: Call ssh-connect ARGS or SHELL
|
||||
.PHONY: ssh
|
||||
ssh: # ssh-get-PrivateIpAddress-$(SERVER_NAME) ## Connect to first remote host
|
||||
$(call ssh-connect,$(AWS_INSTANCE_IP),$(if $(ARGS),$(ARGS),$(SHELL)))
|
||||
ssh: ssh-hosts-check ## Connect to first remote host
|
||||
$(call ssh-connect,$(SSH_HOSTS),$(if $(ARGS),$(ARGS),$(SHELL)))
|
||||
|
||||
# target ssh-add: Fire ssh-key and ssh-add file SSH_PRIVATE_KEYS in folder SSH_DIR
|
||||
.PHONY: ssh-add
|
||||
@@ -15,8 +30,8 @@ ssh-add: ssh-key
|
||||
|
||||
# target ssh-connect: Call ssh-connect make connect SERVICE
|
||||
.PHONY: ssh-connect
|
||||
ssh-connect: # ssh-get-PrivateIpAddress-$(SERVER_NAME)
|
||||
$(call ssh-connect,$(AWS_INSTANCE_IP),make connect COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME) ENV=$(ENV) $(if $(SERVICE),SERVICE=$(SERVICE)))
|
||||
ssh-connect: ssh-hosts-check
|
||||
$(call ssh-connect,$(SSH_HOSTS),make connect COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME) ENV=$(ENV) $(if $(SERVICE),SERVICE=$(SERVICE)))
|
||||
|
||||
# target ssh-del: ssh-add -d file SSH_PRIVATE_KEYS in folder SSH_DIR
|
||||
.PHONY: ssh-del
|
||||
@@ -26,12 +41,8 @@ ssh-del:
|
||||
|
||||
# target ssh-exec: Call ssh-exec make exec SERVICE ARGS
|
||||
.PHONY: ssh-exec
|
||||
ssh-exec: # ssh-get-PrivateIpAddress-$(SERVER_NAME)
|
||||
$(call ssh-exec,$(AWS_INSTANCE_IP),make exec COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME) ENV=$(ENV) $(if $(SERVICE),SERVICE=$(SERVICE)) $(if $(ARGS),ARGS='\''"$(ARGS)"'\''))
|
||||
|
||||
# target ssh-get-PrivateIpAddress-%: Fire aws-ec2-get-instances-PrivateIpAddress-%
|
||||
.PHONY: ssh-get-PrivateIpAddress-%
|
||||
ssh-get-PrivateIpAddress-%: aws-ec2-get-instances-PrivateIpAddress-%;
|
||||
ssh-exec: ssh-hosts-check
|
||||
$(call ssh-exec,$(SSH_HOSTS),make exec COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME) ENV=$(ENV) $(if $(SERVICE),SERVICE=$(SERVICE)) $(if $(ARGS),ARGS='\''"$(ARGS)"'\''))
|
||||
|
||||
# target ssh-key: Add ssh private key SSH_KEY to SSH_DIR
|
||||
.PHONY: ssh-key
|
||||
@@ -43,5 +54,5 @@ endif
|
||||
|
||||
# target ssh-run: Call ssh-run make run SERVICE ARGS
|
||||
.PHONY: ssh-run
|
||||
ssh-run: # ssh-get-PrivateIpAddress-$(SERVER_NAME)
|
||||
$(call ssh-exec,$(AWS_INSTANCE_IP),make run $(if $(SERVICE),SERVICE=$(SERVICE)) $(if $(ARGS),ARGS='\''"$(ARGS)"'\''))
|
||||
ssh-run: ssh-hosts-check
|
||||
$(call ssh-exec,$(SSH_HOSTS),make run $(if $(SERVICE),SERVICE=$(SERVICE)) $(if $(ARGS),ARGS='\''"$(ARGS)"'\''))
|
||||
|
||||
+8
-2
@@ -141,7 +141,9 @@ MACHINE ?= $(shell uname -m 2>/dev/null)
|
||||
ifeq ($(SYSTEM),Darwin)
|
||||
SED_SUFFIX := ''
|
||||
STAT_FORMAT_ARG := -f
|
||||
STAT_FORMAT_FILE := '%a %N'
|
||||
# %m is the modification time; %a is the access time, which is what this used
|
||||
# to ask for, so newer/older did not mean the same thing as on linux
|
||||
STAT_FORMAT_FILE := '%m %N'
|
||||
else
|
||||
STAT_FORMAT_ARG := -c
|
||||
STAT_FORMAT_FILE := '%Y %n'
|
||||
@@ -199,6 +201,9 @@ rs256 = $(shell echo -n '$(1)' |openssl dgst -sha256 -binary -sign '$(2)')
|
||||
JWT_HEADER = {"alg":"HS256","typ":"JWT"}
|
||||
|
||||
# macro JWT: Print Json Web Token for header $1 payload $2 and key $3
|
||||
## a payload is JSON and holds commas, which make read as argument separators,
|
||||
## so the token came out with an empty payload. Pass the payload in a variable
|
||||
## and name it here rather than inlining it.
|
||||
JWT := $(strip \
|
||||
$(eval header := $(or $(1),$(JWT_HEADER))) \
|
||||
$(eval payload := $(or $(2),$(JWT_PAYLOAD))) \
|
||||
@@ -284,7 +289,8 @@ sed = $(RUN) sed -i $(SED_SUFFIX) '$(1)' $(2)
|
||||
verle = [ -n "$(1)" ] && [ "$(1)" = "$(shell echo -e "$(1)\n$(2)" |sort -V |head -n1)" ]
|
||||
|
||||
# macro verlt: Return true when version 1 < 2
|
||||
verlt = [ "$(1)" = "$(2)" ] && return 1 || $(call verlte,$(1),$(2))
|
||||
## it was calling verlte, which does not exist, and returning from no function
|
||||
verlt = [ "$(1)" != "$(2)" ] && $(call verle,$(1),$(2))
|
||||
|
||||
# function conf: Extract variable=value line from configuration files
|
||||
## it prints the line with variable 3 definition from block 2 in file 1
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
set -eu
|
||||
|
||||
# define MYOS path
|
||||
MYOS="$(dirname "$(readlink "$0" || echo "$0")")"
|
||||
## readlink without -f only followed one level and only an absolute link, so a
|
||||
## relative or chained symlink pointed the framework at the wrong directory
|
||||
MYOS="$0"
|
||||
while [ -L "$MYOS" ]; do
|
||||
_link="$(readlink "$MYOS")"
|
||||
case "$_link" in /*) MYOS="$_link" ;; *) MYOS="$(dirname "$MYOS")/$_link" ;; esac
|
||||
done
|
||||
MYOS="$(cd "$(dirname "$MYOS")" && pwd -P)"
|
||||
|
||||
# load system config: /etc/conf.d/myos (openrc convention) first, then the
|
||||
# debian-style /etc/default/myos as fallback
|
||||
@@ -17,4 +24,8 @@ MYOS_CONF=/etc/conf.d/myos
|
||||
|
||||
# call myos Makefile: a WORKDIR from the config or environment wins over PWD,
|
||||
# so a machine can pin its deployment dir and run myos from anywhere
|
||||
IFS=$'\n'; exec env $(cat "$MYOS_CONF" 2>/dev/null) MYOS=. WORKDIR="${WORKDIR:-${PWD}}" make -esC "${MYOS:-.}" "$@"
|
||||
## a comment or a blank line in the config used to become the program env(1)
|
||||
## was asked to run, so the whole command failed
|
||||
IFS=$'\n'
|
||||
exec env $(sed -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' "$MYOS_CONF" 2>/dev/null) \
|
||||
MYOS=. WORKDIR="${WORKDIR:-${PWD}}" make -esC "${MYOS:-.}" "$@"
|
||||
|
||||
+11
-4
@@ -36,10 +36,17 @@ define make
|
||||
$(MYOS_BIN) $(MYOS_ARGS) $(1)
|
||||
endef
|
||||
|
||||
# function myos-var: the value myos resolves for a variable.
|
||||
# A stack keeps its settings in hooks that only myos reads, so a .mk target
|
||||
# asks for them rather than defining them itself:
|
||||
# $(call myos-var,HOST_DOCKER_VOLUME)
|
||||
# The settings of the stacks, read once and evaluated here.
|
||||
# A stack keeps its settings in hooks that only myos reads; asking for them one
|
||||
# at a time costs a process per variable, so they come in a single call.
|
||||
## Written while this file is read, not as a target: a target would collide
|
||||
## with the catch-all rule at the bottom, which hands anything else to myos.
|
||||
MYOS_SETTINGS ?= .myos.settings.mk
|
||||
MYOS_SETTINGS_FILE := $(shell $(MYOS_BIN) --color=never $(MYOS_ARGS) export --make > $(MYOS_SETTINGS) 2>/dev/null && echo $(MYOS_SETTINGS))
|
||||
-include $(MYOS_SETTINGS_FILE)
|
||||
|
||||
# function myos-var: the value myos resolves for one variable, when a single
|
||||
# lookup is cheaper than the whole set
|
||||
myos-var = $(shell $(MYOS_BIN) --color=never $(MYOS_ARGS) env $(1) | awk '{print $$2}')
|
||||
|
||||
# target help: List the myos commands
|
||||
|
||||
@@ -82,6 +82,10 @@ See `references/conventions.md`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Check what a stack opens before starting it on a server that faces the
|
||||
internet: `myos expose <stack>`. A port shown on `0.0.0.0` answers the world,
|
||||
and on linux the host firewall does not see it, because docker writes its own
|
||||
rules. Bind it instead: `ports: ["${MYOS_BIND_PRIVATE}::<port>"]`.
|
||||
- Never run `myos clean` on a host stack: it removes images **and volumes**,
|
||||
including the certificates.
|
||||
- Secrets belong in a file outside the repository, never in a compose file.
|
||||
|
||||
@@ -27,6 +27,8 @@ myos [options] <command> [stack...] [VAR=value...] [-- args...]
|
||||
| `ls [--groups]` | the stacks and groups myos can see |
|
||||
| `env [VAR...]` | resolved variables |
|
||||
| `env-update` | fill the `.env` from the `.env.dist` templates |
|
||||
| `expose [--strict]` | what the stacks publish, and to whom |
|
||||
| `export [--make]` | every setting of the stacks, as `KEY=value` |
|
||||
| `doctor` | check the installation |
|
||||
| `version` | the myos version |
|
||||
|
||||
|
||||
@@ -168,6 +168,52 @@ A line may refer to a variable defined further down. A variable that already
|
||||
has a value keeps it: the `.env` records choices, it never overwrites them, and
|
||||
running the command twice changes nothing.
|
||||
|
||||
## What a stack publishes, and to whom
|
||||
|
||||
On linux docker writes its own firewall rules, so a port published with
|
||||
`ports: ["8080:80"]` answers the internet whatever the host firewall says. The
|
||||
portable answer is to publish where you mean to, which behaves the same on
|
||||
linux and on macOS and needs no privilege:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
ports:
|
||||
- "${MYOS_BIND_PRIVATE}::8080" # this host only, reached through fabio
|
||||
gateway:
|
||||
ports:
|
||||
- "${MYOS_BIND_PUBLIC}:443:443" # the internet, on purpose
|
||||
peer:
|
||||
ports:
|
||||
- "${MYOS_BIND_MESH}::7946" # the private network between the hosts
|
||||
```
|
||||
|
||||
| scope | address | for |
|
||||
|---|---|---|
|
||||
| `private` | `127.0.0.1` | everything the load balancer reaches for you. The default. |
|
||||
| `public` | `0.0.0.0` | a load balancer, a public DNS or mail service |
|
||||
| `mesh` | the mesh interface | services shared between the hosts of a fleet |
|
||||
|
||||
`MYOS_BIND_PUBLIC`, `MYOS_BIND_PRIVATE` and `MYOS_BIND_MESH` override the
|
||||
addresses; `MYOS_MESH_IFACE` names the interface when it is not one of
|
||||
easytier, tun0, tailscale0, mycelium or wg0.
|
||||
|
||||
A stack also declares what it means, so an audit can tell a deliberate choice
|
||||
from an oversight:
|
||||
|
||||
```sh
|
||||
<PREFIX>_SERVICE_EXPOSE=public # the whole stack
|
||||
<PREFIX>_SERVICE_443_EXPOSE=public # one port
|
||||
```
|
||||
|
||||
`<PREFIX>` is `HOST_<name>` for a host stack, `USER_<name>` for a user stack,
|
||||
`<name>` otherwise.
|
||||
|
||||
```sh
|
||||
myos expose # what each stack publishes, and its declared scope
|
||||
myos expose --strict # exits 1 when a port faces the world undeclared
|
||||
```
|
||||
|
||||
## Groups
|
||||
|
||||
A group is a lowercase name whose value lists stacks. It can live in a `.env`,
|
||||
|
||||
@@ -84,3 +84,10 @@ shim-env | shim-project | @make env ARGS=COMPOSE_PROJECT
|
||||
chain-build-up-logs | host-project | build up logs host/fabio
|
||||
chain-up-group | host-project | up ps host
|
||||
chain-print-two | host-project | print-COMPOSE_PROJECT_NAME print-APP STACK=host/consul
|
||||
mk-ssh-no-hosts | app-nogit | ssh
|
||||
mk-ssh-with-hosts | app-nogit | ssh SSH_HOSTS=example.test ARGS=id
|
||||
bridge-export | host-project | export STACK=host/consul
|
||||
cmd-recreate | host-project | recreate host/consul
|
||||
cmd-status | host-project | status host/consul
|
||||
expose-none | host-project | expose host/consul
|
||||
expose-default | app-git | expose
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
[exit 0]
|
||||
@@ -0,0 +1,4 @@
|
||||
docker network create tester-local
|
||||
docker network create testhost
|
||||
docker compose -f @WD@/stack/host/consul.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host up -d --force-recreate
|
||||
[exit 0]
|
||||
@@ -0,0 +1,2 @@
|
||||
docker compose -f @WD@/stack/host/consul.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host ps
|
||||
[exit 0]
|
||||
@@ -0,0 +1,2 @@
|
||||
no published port: nothing is reachable from outside the docker network
|
||||
[exit 0]
|
||||
@@ -0,0 +1,2 @@
|
||||
no published port: nothing is reachable from outside the docker network
|
||||
[exit 0]
|
||||
@@ -0,0 +1,3 @@
|
||||
ERROR: unknown command: ssh
|
||||
ERROR: to act on a stack of that name, say what to do: myos up ssh
|
||||
[exit 2]
|
||||
@@ -0,0 +1,3 @@
|
||||
ERROR: unknown command: ssh
|
||||
ERROR: to act on a stack of that name, say what to do: myos up ssh
|
||||
[exit 2]
|
||||
@@ -0,0 +1,2 @@
|
||||
WARNING: myos[0] export-rule-exists: target export unavailable in app myos
|
||||
[exit 0]
|
||||
@@ -0,0 +1,7 @@
|
||||
make -o docker-stack-recreate MAKE_OLDFILE=docker-stack-recreate ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-recreate STACK=myos APP_NAME=myos
|
||||
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local rm -fs
|
||||
sh -c docker network create tester-local >/dev/null 2>&1
|
||||
sh -c docker network create testhost >/dev/null 2>&1
|
||||
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local up -d
|
||||
WARNING: myos[0] host/consul-rule-exists: target host/consul unavailable in app myos
|
||||
[exit 0]
|
||||
@@ -0,0 +1,4 @@
|
||||
make -o docker-stack-ps MAKE_OLDFILE=docker-stack-ps ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-ps STACK=myos APP_NAME=myos
|
||||
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local ps
|
||||
WARNING: myos[0] host/consul-rule-exists: target host/consul unavailable in app myos
|
||||
[exit 0]
|
||||
@@ -0,0 +1,2 @@
|
||||
WARNING: myos[0] expose-rule-exists: target expose unavailable in app myos
|
||||
[exit 0]
|
||||
@@ -0,0 +1,3 @@
|
||||
WARNING: myos[0] expose-rule-exists: target expose unavailable in app myos
|
||||
WARNING: myos[0] host/consul-rule-exists: target host/consul unavailable in app myos
|
||||
[exit 0]
|
||||
@@ -0,0 +1,3 @@
|
||||
ERROR: myos[0] ssh-hosts-check: no remote host: set SSH_HOSTS=host1 host2
|
||||
make: *** [ssh-hosts-check] Error 2
|
||||
[exit 2]
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
ERROR: myos[0] ssh: Unable to find docker tester-example-test
|
||||
make: *** [ssh] Error 2
|
||||
[exit 2]
|
||||
@@ -0,0 +1,79 @@
|
||||
#shellcheck shell=sh
|
||||
Include lib/core.sh
|
||||
Include lib/str.sh
|
||||
Include lib/var.sh
|
||||
Include lib/tags.sh
|
||||
Include lib/naming.sh
|
||||
Include lib/stack.sh
|
||||
Include lib/expose.sh
|
||||
|
||||
# Docker writes its own firewall rules, so a port published to 0.0.0.0 answers
|
||||
# the internet whatever the host firewall says. Binding the publication is the
|
||||
# portable answer: it behaves the same on linux and on macOS, without root.
|
||||
Describe 'lib/expose.sh'
|
||||
Describe 'myos_bind'
|
||||
It 'binds the private scope to the loopback'
|
||||
When call myos_bind private
|
||||
The output should equal "127.0.0.1"
|
||||
End
|
||||
It 'binds the public scope to every address'
|
||||
When call myos_bind public
|
||||
The output should equal "0.0.0.0"
|
||||
End
|
||||
It 'takes an explicit address over the default'
|
||||
MYOS_BIND_PRIVATE=10.0.0.1
|
||||
When call myos_bind private
|
||||
The output should equal "10.0.0.1"
|
||||
End
|
||||
It 'keeps an unknown scope private rather than public'
|
||||
When call myos_bind nonsense
|
||||
The output should equal "127.0.0.1"
|
||||
End
|
||||
It 'falls back to the private address when there is no mesh'
|
||||
MYOS_MESH_IFACE=nosuchiface0
|
||||
When call myos_bind mesh
|
||||
The output should equal "127.0.0.1"
|
||||
End
|
||||
It 'uses the mesh address when one is given'
|
||||
MYOS_BIND_MESH=10.144.0.2
|
||||
When call myos_bind mesh
|
||||
The output should equal "10.144.0.2"
|
||||
End
|
||||
End
|
||||
|
||||
Describe 'myos_stack_prefix'
|
||||
Parameters
|
||||
"host/fabio" "HOST_FABIO"
|
||||
"User/ipfs" "USER_IPFS"
|
||||
"supabase" "SUPABASE"
|
||||
"drone/drone" "DRONE"
|
||||
End
|
||||
It "prefixes the settings of $1 with $2"
|
||||
When call myos_stack_prefix "$1"
|
||||
The output should equal "$2"
|
||||
End
|
||||
End
|
||||
|
||||
Describe 'myos_expose_scope'
|
||||
It 'is private unless the stack says otherwise'
|
||||
When call myos_expose_scope HOST_FTPS ftps 21
|
||||
The output should equal "private"
|
||||
End
|
||||
It 'reads the scope of one port'
|
||||
HOST_FTPS_SERVICE_21_EXPOSE=public
|
||||
When call myos_expose_scope HOST_FTPS ftps 21
|
||||
The output should equal "public"
|
||||
End
|
||||
It 'reads the scope of a whole stack'
|
||||
HOST_FTPS_SERVICE_EXPOSE=mesh
|
||||
When call myos_expose_scope HOST_FTPS ftps 21
|
||||
The output should equal "mesh"
|
||||
End
|
||||
It 'prefers the port over the stack'
|
||||
HOST_FTPS_SERVICE_EXPOSE=mesh
|
||||
HOST_FTPS_SERVICE_21_EXPOSE=public
|
||||
When call myos_expose_scope HOST_FTPS ftps 21
|
||||
The output should equal "public"
|
||||
End
|
||||
End
|
||||
End
|
||||
Reference in New Issue
Block a user