Compare commits
4
Commits
90bb97cca8
...
posix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
803dcd1e10 | ||
|
|
ed3c5a0c7c | ||
|
|
f429b8c38d | ||
|
|
be777fc9e6 |
+6
-1
@@ -24,7 +24,12 @@
|
|||||||
fails when a port faces the world without saying so. `MYOS_BIND_PUBLIC`,
|
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
|
`_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
|
the linux-only ufw-docker patching with something that behaves the same on
|
||||||
macOS and needs no privilege
|
macOS and needs no privilege. The scope is read from the compose file rather
|
||||||
|
than declared beside it, so it cannot disagree with what is published
|
||||||
|
- `myos cert` derives the certificates a server needs from the route tags its
|
||||||
|
stacks publish, and asks dehydrated for them: a wildcard where a tag uses one,
|
||||||
|
a certificate per name otherwise. The `host/dehydrated` stack answers http-01
|
||||||
|
itself and delegates dns-01 to a provider hook
|
||||||
- commands chain: `myos build up logs host/fabio`, as make targets did
|
- 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
|
- the stack catalogue no longer needs make at all: its settings are hooks, and
|
||||||
only six stacks keep a .mk, for targets
|
only six stacks keep a .mk, for targets
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Restart protocol (2026-09-05)
|
||||||
|
|
||||||
|
The rewrite starts over from the make engine, not from `lib/`.
|
||||||
|
|
||||||
|
1. **Reference = `make/*` as it is**, bugs fixed one by one, each with a test
|
||||||
|
that goes red then green. Known bugs and their locations are listed in the
|
||||||
|
planning notes and in `git log --grep=fix` on this branch.
|
||||||
|
2. **Tests first, against make**: every target in scope gets a functional case
|
||||||
|
in `spec/golden/cases.txt`, recorded from the make engine
|
||||||
|
(`spec/golden/record.sh`, `spec/support/run.sh` engine `legacy`). That
|
||||||
|
recording is the historical behaviour, defects included.
|
||||||
|
3. **Rewrite target by target in red/green**: the new implementation (`just`
|
||||||
|
as the interface with line recipes, POSIX sh as the logic) must turn each
|
||||||
|
case green under `MYOS_ENGINE=just`. A deliberate departure from the
|
||||||
|
historical behaviour is written down in `spec/golden/DELTAS.md`.
|
||||||
|
4. The first attempt (`bin/myos`, `lib/`, `lib/cmd/`, `share/make/shim.mk`,
|
||||||
|
`spec/golden/expected.cli/`, `spec/unit/`) is tagged `attempt-1-lib` and is
|
||||||
|
not a base for the rewrite. What is worth keeping from it is ideas: port
|
||||||
|
exposure by bind address (`expose --strict`), certificates derived from the
|
||||||
|
route tags (dehydrated), `env-update` with forward references, stack
|
||||||
|
directories merged along the search path, lazy defaults, command chaining,
|
||||||
|
typed exit codes, the agent skill, the installer.
|
||||||
|
5. The catalogue readable by make is `myos-stacks@7289b83` (or `github/develop`
|
||||||
|
here): the later hooks (`_stack.sh`) are not read by make.
|
||||||
|
6. Scope from real fleet usage (~15 targets): up down build config logs ps
|
||||||
|
restart status, the `host` group, print-VAR, docker-build-<image>,
|
||||||
|
setup-ufw, install bootstrap clean, apps-install. Never used: release,
|
||||||
|
subrepo, git-*, deploy, ssh-*.
|
||||||
|
7. Keep and reuse: `spec/support/run.sh`, the docker mocks, the fixtures, the
|
||||||
|
golden cases, `spec/bench/` (make 312 ms fixed + ~700 ms per stack; just
|
||||||
|
line recipe 19 ms, shebang recipe 160 ms; shell hooks with command
|
||||||
|
substitutions ~40 ms per computed setting on any engine).
|
||||||
|
|
||||||
|
Traps already paid for in POSIX sh, do not pay them again: `IFS=$'\n'` stops
|
||||||
|
argument splitting; `for w in $list` globs a `*` (use `set -f`); `[a-z]`
|
||||||
|
matches uppercase under fr_FR (use `[:lower:]`); a function called inside
|
||||||
|
`$( )` cannot return through a global; an environment variable must never be
|
||||||
|
taken for a stack group (lowercase names only); an unprefixed lazy default
|
||||||
|
named `host` runs `/usr/bin/host`; zsh does not split unquoted variables, so
|
||||||
|
test scripts run under `sh`.
|
||||||
@@ -19,7 +19,7 @@ done
|
|||||||
MYOS_ROOT=$(cd "$(dirname "$_self")/.." && pwd -P)
|
MYOS_ROOT=$(cd "$(dirname "$_self")/.." && pwd -P)
|
||||||
export MYOS_ROOT
|
export MYOS_ROOT
|
||||||
|
|
||||||
for _m in core str var tags naming stack config compose hooks context expose; do
|
for _m in core str var tags naming stack config compose hooks context expose cert; do
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
. "$MYOS_ROOT/lib/$_m.sh"
|
. "$MYOS_ROOT/lib/$_m.sh"
|
||||||
done
|
done
|
||||||
@@ -70,6 +70,7 @@ Commands:
|
|||||||
export every setting of the stacks, as KEY=value
|
export every setting of the stacks, as KEY=value
|
||||||
env-update fill .env from the .env.dist templates
|
env-update fill .env from the .env.dist templates
|
||||||
expose [--strict] what the stacks publish, and to whom
|
expose [--strict] what the stacks publish, and to whom
|
||||||
|
cert list|issue|renew|show certificates, derived from the routes
|
||||||
doctor check the installation
|
doctor check the installation
|
||||||
version print the myos version
|
version print the myos version
|
||||||
|
|
||||||
@@ -148,6 +149,16 @@ for _c in $MYOS_CMDS; do
|
|||||||
done
|
done
|
||||||
MYOS_CMDS=$_cmds
|
MYOS_CMDS=$_cmds
|
||||||
|
|
||||||
|
# cert takes a subcommand where the others take only stacks
|
||||||
|
case $MYOS_CMDS in
|
||||||
|
*cert*)
|
||||||
|
case ${MYOS_REFS%% *} in
|
||||||
|
list|domains|issue|renew|show)
|
||||||
|
MYOS_VARS=${MYOS_REFS%% *}
|
||||||
|
case $MYOS_REFS in *' '*) MYOS_REFS=${MYOS_REFS#* } ;; *) MYOS_REFS= ;; esac ;;
|
||||||
|
esac ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# env, ls and doctor take variable names where the others take stacks
|
# env, ls and doctor take variable names where the others take stacks
|
||||||
case $MYOS_CMDS in
|
case $MYOS_CMDS in
|
||||||
env|ls|doctor)
|
env|ls|doctor)
|
||||||
|
|||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#shellcheck shell=sh
|
||||||
|
# cert: which certificates a server needs, derived from what its stacks route.
|
||||||
|
#
|
||||||
|
# The hostnames are already declared, once, in the fabio route tags a stack
|
||||||
|
# publishes: urlprefix-<host>/<path>. Asking for them a second time in a
|
||||||
|
# domains.txt would be a second source of truth, free to disagree with what is
|
||||||
|
# actually served. They are read from the resolved compose configuration
|
||||||
|
# instead.
|
||||||
|
#
|
||||||
|
# A name written *.example.org needs a wildcard, which ACME only issues over
|
||||||
|
# dns-01; a concrete name can be had over http-01. That is the whole of
|
||||||
|
# "per-site or wildcard according to need": the tags say which.
|
||||||
|
|
||||||
|
# myos_cert_names the hostnames the requested stacks route, one per line
|
||||||
|
myos_cert_names() {
|
||||||
|
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
|
||||||
|
done | myos_cert_parse
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_parse (compose config on stdin) -> hostnames
|
||||||
|
# A tag is urlprefix-<host>[:<port>]/<path> with options after a space; the
|
||||||
|
# bare "*" is fabio's catch-all and names nothing.
|
||||||
|
myos_cert_parse() {
|
||||||
|
grep -oE 'urlprefix-[^",[:space:]]*' 2>/dev/null |
|
||||||
|
sed -e 's/^urlprefix-//' -e 's|/.*||' -e 's/:[0-9]*$//' |
|
||||||
|
grep -vE '^\*?$' |
|
||||||
|
sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_covers WILDCARD_PARENT NAME does *.parent cover this name?
|
||||||
|
# A wildcard matches one label, so *.example.org covers a.example.org but
|
||||||
|
# neither example.org nor a.b.example.org.
|
||||||
|
myos_cert_covers() {
|
||||||
|
case $2 in
|
||||||
|
*".$1")
|
||||||
|
_head=${2%".$1"}
|
||||||
|
case $_head in *.*|'') return 1 ;; *) return 0 ;; esac ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_groups the certificates to ask for, one per line, in the shape
|
||||||
|
# dehydrated reads: the common name first, then its subject alternative names.
|
||||||
|
#
|
||||||
|
# MYOS_CERT_MODE:
|
||||||
|
# auto a wildcard where the tags use one, a certificate per name otherwise
|
||||||
|
# wildcard one wildcard per domain, whether or not a tag asked for it
|
||||||
|
# per-site never a wildcard: one certificate per name, dns-01 not required
|
||||||
|
myos_cert_groups() {
|
||||||
|
_names=$(myos_cert_names)
|
||||||
|
[ -n "$_names" ] || return 0
|
||||||
|
_mode=${MYOS_CERT_MODE:-auto}
|
||||||
|
|
||||||
|
# the parents a wildcard is wanted for
|
||||||
|
_wild=
|
||||||
|
for _n in $_names; do
|
||||||
|
case $_n in
|
||||||
|
\*.*) [ "$_mode" = per-site ] || _wild="$_wild ${_n#\*.}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "$_mode" = wildcard ]; then
|
||||||
|
for _n in $_names; do
|
||||||
|
case $_n in
|
||||||
|
\*.*) ;;
|
||||||
|
*.*.*) _wild="$_wild ${_n#*.}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
_wild=$(printf '%s' "$_wild" | tr ' ' '\n' | sed '/^$/d' | sort -u)
|
||||||
|
|
||||||
|
# one line per wildcard, the parent first so it is the common name
|
||||||
|
for _p in $_wild; do
|
||||||
|
printf '%s *.%s\n' "$_p" "$_p"
|
||||||
|
done
|
||||||
|
|
||||||
|
# the concrete names a wildcard does not already cover
|
||||||
|
for _n in $_names; do
|
||||||
|
case $_n in \*.*) continue ;; esac
|
||||||
|
_covered=no
|
||||||
|
for _p in $_wild; do
|
||||||
|
[ "$_n" = "$_p" ] && { _covered=yes; break; }
|
||||||
|
myos_cert_covers "$_p" "$_n" && { _covered=yes; break; }
|
||||||
|
done
|
||||||
|
[ "$_covered" = no ] && printf '%s\n' "$_n"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_needs_dns true when any certificate asked for is a wildcard
|
||||||
|
myos_cert_needs_dns() { myos_cert_groups | grep -q '\*\.'; }
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#shellcheck shell=sh
|
||||||
|
# shellcheck disable=SC1091 # lib/cmd files are sourced by path at run time
|
||||||
|
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
|
||||||
|
# myos cert <list|domains|issue|renew|show> the certificates a server needs
|
||||||
|
#
|
||||||
|
# The hostnames come from the route tags of the stacks, so a site gets a
|
||||||
|
# certificate by being routed, not by being written down a second time.
|
||||||
|
myos_cmd_cert() {
|
||||||
|
_sub=$(myos_firstword "${MYOS_VARS:-}${MYOS_ARGS:+ $MYOS_ARGS}")
|
||||||
|
[ -n "$_sub" ] || _sub=list
|
||||||
|
case $_sub in
|
||||||
|
list) myos_cert_list ;;
|
||||||
|
domains) myos_cert_write_domains ;;
|
||||||
|
issue) myos_cert_run "" ;;
|
||||||
|
renew) myos_cert_run "--cron" ;;
|
||||||
|
show) myos_cert_show ;;
|
||||||
|
*) myos_die "$MYOS_E_USAGE" "myos cert <list|domains|issue|renew|show>" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_list the certificates that would be asked for, and how
|
||||||
|
myos_cert_list() {
|
||||||
|
_groups=$(myos_cert_groups)
|
||||||
|
[ -n "$_groups" ] || {
|
||||||
|
printf 'no routed hostname: nothing to certify\n'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
printf '%s%-46s %-9s %s%s\n' "$MYOS_C_HIGHLIGHT" CERTIFICATE CHALLENGE NAMES "$MYOS_C_RESET"
|
||||||
|
printf '%s\n' "$_groups" | while IFS= read -r _line; do
|
||||||
|
_cn=$(myos_firstword "$_line")
|
||||||
|
case $_line in
|
||||||
|
*'*.'*) _ch=dns-01 ;;
|
||||||
|
*) _ch=http-01 ;;
|
||||||
|
esac
|
||||||
|
printf '%-46s %-9s %s\n' "$_cn" "$_ch" "$_line"
|
||||||
|
done
|
||||||
|
myos_cert_needs_dns &&
|
||||||
|
myos_info "a wildcard is asked for: dns-01 needs MYOS_CERT_HOOK to talk to your dns provider"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_write_domains the domains.txt dehydrated reads
|
||||||
|
myos_cert_write_domains() {
|
||||||
|
_dir=${MYOS_CERT_DIR:-$WORKDIR/.myos/dehydrated}
|
||||||
|
_file=$_dir/domains.txt
|
||||||
|
_groups=$(myos_cert_groups)
|
||||||
|
[ -n "$_groups" ] || { myos_warning "no routed hostname: not writing $_file"; return 0; }
|
||||||
|
myos_run mkdir -p "$_dir"
|
||||||
|
if [ "${DRYRUN:-false}" = true ]; then
|
||||||
|
printf 'would write %s:\n%s\n' "$_file" "$_groups"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$_groups" > "$_file"
|
||||||
|
printf '%s\n' "$_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_run ARGS run dehydrated in the host stack, on the domains derived
|
||||||
|
myos_cert_run() {
|
||||||
|
myos_cert_write_domains >/dev/null || return $?
|
||||||
|
_args=$1
|
||||||
|
[ -n "${MYOS_CERT_STAGING:-}" ] && _args="$_args --staging"
|
||||||
|
case ${MYOS_ARGS:-} in
|
||||||
|
*--staging*) _args="$_args --staging" ;;
|
||||||
|
esac
|
||||||
|
case ${MYOS_ARGS:-} in
|
||||||
|
*--force*) _args="$_args --force" ;;
|
||||||
|
esac
|
||||||
|
MYOS_ARGS="$_args" SERVICE=${SERVICE:-dehydrated} \
|
||||||
|
MYOS_STACKS="host/dehydrated" myos_cert_exec
|
||||||
|
}
|
||||||
|
|
||||||
|
myos_cert_exec() {
|
||||||
|
# shellcheck source=lib/cmd/exec.sh
|
||||||
|
. "$MYOS_ROOT/lib/cmd/exec.sh"
|
||||||
|
myos_cmd_exec
|
||||||
|
}
|
||||||
|
|
||||||
|
# myos_cert_show the certificates that exist, and when they expire
|
||||||
|
myos_cert_show() {
|
||||||
|
_vol=${HOST_DOCKER_VOLUME:-${HOSTNAME:-localhost}}
|
||||||
|
# shellcheck disable=SC2016 # the script runs in the container, not here
|
||||||
|
myos_run docker run --rm -v "$_vol:/host" alpine:3.20 sh -c '
|
||||||
|
apk add -q openssl 2>/dev/null
|
||||||
|
for c in /host/certs/*-cert.pem; do
|
||||||
|
[ -f "$c" ] || continue
|
||||||
|
n=$(basename "$c" -cert.pem)
|
||||||
|
e=$(openssl x509 -in "$c" -noout -enddate 2>/dev/null | sed "s/notAfter=//")
|
||||||
|
i=$(openssl x509 -in "$c" -noout -issuer 2>/dev/null | sed "s/.*CN *= *//;s/,.*//")
|
||||||
|
printf "%-46s %-28s %s\n" "$n" "$e" "$i"
|
||||||
|
done'
|
||||||
|
}
|
||||||
+45
-31
@@ -1,52 +1,56 @@
|
|||||||
#shellcheck shell=sh
|
#shellcheck shell=sh
|
||||||
# myos expose [--strict] what the stacks publish, and to whom
|
# myos expose [--strict] what the stacks publish, and to whom
|
||||||
#
|
#
|
||||||
# Reads the resolved compose configuration, so it reports what `myos up` would
|
# Two readings are joined: the compose files as written, which say which
|
||||||
# open rather than what happens to be running. A port bound to 0.0.0.0 answers
|
# binding each port asks for, and the resolved configuration, which says the
|
||||||
# the internet: on linux docker writes its own firewall rules and the host
|
# address it ends up on. The first is the intent, the second is the fact, and
|
||||||
# firewall does not see it. --strict exits 1 when a port is world-bound
|
# reporting both is the point: a port nobody bound answers the internet, and on
|
||||||
# without the stack declaring that scope.
|
# linux the host firewall does not see it, because docker writes its own rules.
|
||||||
|
#
|
||||||
|
# --strict exits 1 when a port is published without a binding.
|
||||||
myos_cmd_expose() {
|
myos_cmd_expose() {
|
||||||
_strict=false
|
_strict=false
|
||||||
case ${MYOS_ARGS:-}${MYOS_VARS:-} in *--strict*) _strict=true ;; esac
|
case ${MYOS_ARGS:-}${MYOS_VARS:-} in *--strict*) _strict=true ;; esac
|
||||||
|
|
||||||
_rows=$(myos_expose_rows)
|
_rows=$(myos_expose_rows)
|
||||||
[ -n "$_rows" ] || { printf 'no published port: nothing is reachable from outside the docker network\n'; return 0; }
|
[ -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' \
|
printf '%s%-20s %-14s %-22s %-6s %s%s\n' \
|
||||||
"$MYOS_C_HIGHLIGHT" STACK SERVICE "PUBLISHED ON" PORT SCOPE "$MYOS_C_RESET"
|
"$MYOS_C_HIGHLIGHT" STACK SERVICE "PUBLISHED ON" PORT BINDING "$MYOS_C_RESET"
|
||||||
_bad=0
|
_bad=0
|
||||||
_oIFS=$IFS; IFS='
|
_oIFS=$IFS; IFS='
|
||||||
'
|
'
|
||||||
for _row in $_rows; do
|
for _row in $_rows; do
|
||||||
IFS=$_oIFS
|
IFS=$_oIFS
|
||||||
_st=${_row%%|*}; _rest=${_row#*|}
|
_st=${_row%%|*}; _r=${_row#*|}
|
||||||
_sv=${_rest%%|*}; _rest=${_rest#*|}
|
_sv=${_r%%|*}; _r=${_r#*|}
|
||||||
_on=${_rest%%|*}; _rest=${_rest#*|}
|
_on=${_r%%|*}; _r=${_r#*|}
|
||||||
_pt=${_rest%%|*}; _sc=${_rest#*|}
|
_pt=${_r%%|*}; _sc=${_r#*|}
|
||||||
case ${_on%:*} in
|
if [ "$_sc" = unbound ]; then
|
||||||
0.0.0.0|''|'::'|'*')
|
_bad=$((_bad + 1))
|
||||||
[ "$_sc" = public ] || _bad=$((_bad + 1))
|
printf '%-20s %-14s %s%-22s%s %-6s %s%s%s\n' "$_st" "$_sv" \
|
||||||
printf '%-20s %-14s %s%-22s%s %-6s %s\n' \
|
"$MYOS_C_WARN" "$_on" "$MYOS_C_RESET" "$_pt" "$MYOS_C_WARN" "$_sc" "$MYOS_C_RESET"
|
||||||
"$_st" "$_sv" "$MYOS_C_WARN" "$_on" "$MYOS_C_RESET" "$_pt" "$_sc" ;;
|
else
|
||||||
*)
|
printf '%-20s %-14s %-22s %-6s %s\n' "$_st" "$_sv" "$_on" "$_pt" "$_sc"
|
||||||
printf '%-20s %-14s %-22s %-6s %s\n' "$_st" "$_sv" "$_on" "$_pt" "$_sc" ;;
|
fi
|
||||||
esac
|
|
||||||
IFS='
|
IFS='
|
||||||
'
|
'
|
||||||
done
|
done
|
||||||
IFS=$_oIFS
|
IFS=$_oIFS
|
||||||
|
|
||||||
if [ "$_bad" -gt 0 ]; then
|
if [ "$_bad" -gt 0 ]; then
|
||||||
myos_warning "$_bad port(s) reachable from anywhere without declaring the public scope"
|
myos_warning "$_bad port(s) published without a binding: docker opens them on every address"
|
||||||
myos_warning "bind them: ports: [\"\${MYOS_BIND_PRIVATE}::<port>\"]"
|
# shellcheck disable=SC2016 # the variable name is the message, not a value
|
||||||
|
myos_warning 'bind them: ports: ["${MYOS_BIND_PRIVATE}::<port>"] for a service behind the load balancer'
|
||||||
[ "$_strict" = true ] && return "$MYOS_E_FAIL"
|
[ "$_strict" = true ] && return "$MYOS_E_FAIL"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# myos_expose_rows STACK|SERVICE|ADDR:PORT|CONTAINER_PORT|SCOPE for every
|
# myos_expose_rows STACK|SERVICE|ADDR:PORT|CONTAINER_PORT|BINDING
|
||||||
# published port of the requested stacks
|
|
||||||
myos_expose_rows() {
|
myos_expose_rows() {
|
||||||
for _ref in $MYOS_STACKS; do
|
for _ref in $MYOS_STACKS; do
|
||||||
_files=$(myos_stack_compose_files "$_ref" 2>/dev/null) || continue
|
_files=$(myos_stack_compose_files "$_ref" 2>/dev/null) || continue
|
||||||
@@ -56,15 +60,26 @@ myos_expose_rows() {
|
|||||||
$_fw"
|
$_fw"
|
||||||
_app=$(myos_stack_name "$_ref")
|
_app=$(myos_stack_name "$_ref")
|
||||||
_project=$(myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$_app")
|
_project=$(myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$_app")
|
||||||
|
|
||||||
|
# what the files ask for, later overlays overriding earlier ones
|
||||||
|
_decl=$(mktemp "${TMPDIR:-/tmp}/myos-expose.XXXXXX")
|
||||||
|
# shellcheck disable=SC2086 # a newline separated list of paths
|
||||||
|
myos_expose_declared $_files > "$_decl" 2>/dev/null
|
||||||
|
|
||||||
DRYRUN=false myos_compose "$_project" "$_files" -- config 2>/dev/null |
|
DRYRUN=false myos_compose "$_project" "$_files" -- config 2>/dev/null |
|
||||||
myos_expose_parse "$_ref" "$(myos_stack_prefix "$_ref")"
|
myos_expose_resolved |
|
||||||
|
while IFS='|' read -r _v _t _o; do
|
||||||
|
_b=$(awk -F'|' -v s="$_v" -v p="$_t" '$1==s && $2==p {last=$3} END {print last}' "$_decl")
|
||||||
|
printf '%s|%s|%s|%s|%s\n' "$_ref" "$_v" "$_o" "$_t" "${_b:-unbound}"
|
||||||
|
done
|
||||||
|
rm -f "$_decl"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# myos_expose_parse STACK NAME (compose config on stdin)
|
# myos_expose_resolved (compose config on stdin) -> SERVICE|CONTAINER_PORT|ADDR:PORT
|
||||||
# compose normalises every port to the long form, so one shape is enough
|
# compose normalises every port to the long form, so one shape is enough
|
||||||
myos_expose_parse() {
|
myos_expose_resolved() {
|
||||||
awk -v stack="$1" '
|
awk '
|
||||||
/^services:/ { insvc = 1; next }
|
/^services:/ { insvc = 1; next }
|
||||||
insvc && /^ [a-zA-Z0-9_.-]+:/ { svc = $1; sub(/:$/, "", svc); inports = 0 }
|
insvc && /^ [a-zA-Z0-9_.-]+:/ { svc = $1; sub(/:$/, "", svc); inports = 0 }
|
||||||
insvc && /^ ports:/ { inports = 1; next }
|
insvc && /^ ports:/ { inports = 1; next }
|
||||||
@@ -73,10 +88,9 @@ myos_expose_parse() {
|
|||||||
inports && /published:/ { pub = $2; gsub(/"/, "", pub) }
|
inports && /published:/ { pub = $2; gsub(/"/, "", pub) }
|
||||||
inports && /target:/ { tgt = $2 }
|
inports && /target:/ { tgt = $2 }
|
||||||
inports && /protocol:/ {
|
inports && /protocol:/ {
|
||||||
printf "%s|%s|%s:%s|%s\n", stack, svc, (ip == "" ? "0.0.0.0" : ip), pub, tgt
|
# compose leaves published empty when docker picks the port at run time
|
||||||
|
printf "%s|%s|%s:%s\n", svc, tgt, (ip == "" ? "0.0.0.0" : ip), (pub == "" ? "auto" : pub)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-12
@@ -15,7 +15,11 @@
|
|||||||
# mesh the private network between the hosts of the fleet
|
# mesh the private network between the hosts of the fleet
|
||||||
# private this host only: everything the load balancer reaches for you
|
# private this host only: everything the load balancer reaches for you
|
||||||
#
|
#
|
||||||
# MYOS_BIND_<SCOPE> overrides any of them.
|
# A stack does not declare its scope on the side: it is which of these it binds
|
||||||
|
# to, read from the compose file. One source of truth, which cannot drift from
|
||||||
|
# what is actually published. MYOS_BIND_<SCOPE> sets the address of a scope on
|
||||||
|
# a given host, which is the part that belongs to the host rather than to the
|
||||||
|
# stack.
|
||||||
|
|
||||||
# myos_bind SCOPE the address a port of that scope binds to
|
# myos_bind SCOPE the address a port of that scope binds to
|
||||||
myos_bind() {
|
myos_bind() {
|
||||||
@@ -66,15 +70,50 @@ myos_stack_prefix() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# myos_expose_scope PREFIX SERVICE PORT the scope a stack declares for a port:
|
# myos_expose_declared FILE... SERVICE|CONTAINER_PORT|SCOPE for every port a
|
||||||
# <PREFIX>_SERVICE_<port>_EXPOSE, then <PREFIX>_SERVICE_EXPOSE, then the same
|
# compose file publishes, read from the file as written rather than from the
|
||||||
# two on the service name, else private
|
# resolved configuration.
|
||||||
myos_expose_scope() {
|
#
|
||||||
_u=$(myos_upper "$1")
|
# The scope is not declared twice: it is which binding the file asks for.
|
||||||
for _n in "${_u}_SERVICE_${3}_EXPOSE" "${_u}_SERVICE_EXPOSE" \
|
# ${MYOS_BIND_PUBLIC}:443:443 public
|
||||||
"$(myos_upper "$2")_SERVICE_${3}_EXPOSE" "$(myos_upper "$2")_SERVICE_EXPOSE"; do
|
# ${MYOS_BIND_PRIVATE}::8080 private
|
||||||
_s=$(myos_var "$_n")
|
# ${MYOS_BIND_MESH}::7946 mesh
|
||||||
[ -n "$_s" ] && { printf '%s' "$_s"; return 0; }
|
# 127.0.0.1:5432:5432 pinned to an address, deliberate but fixed
|
||||||
done
|
# 80 or 8080:80 unbound: docker binds every address, and
|
||||||
printf 'private'
|
# nobody chose that
|
||||||
|
#
|
||||||
|
# Resolving first would lose the difference: ${MYOS_BIND_PRIVATE} and a
|
||||||
|
# hand-written 127.0.0.1 both become 127.0.0.1, and an unbound port becomes
|
||||||
|
# 0.0.0.0 exactly like a deliberate public one.
|
||||||
|
myos_expose_declared() {
|
||||||
|
awk '
|
||||||
|
function indent(line, n) { match(line, /^ */); return RLENGTH }
|
||||||
|
function emit(entry, e, scope, target, n, parts) {
|
||||||
|
e = entry
|
||||||
|
sub(/^ *- */, "", e)
|
||||||
|
gsub(/^["'"'"']|["'"'"']$/, "", e)
|
||||||
|
if (e ~ /\$\{MYOS_BIND_PUBLIC[^}]*\}/) scope = "public"
|
||||||
|
else if (e ~ /\$\{MYOS_BIND_MESH[^}]*\}/) scope = "mesh"
|
||||||
|
else if (e ~ /\$\{MYOS_BIND_PRIVATE[^}]*\}/) scope = "private"
|
||||||
|
else if (e ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:/) scope = "pinned"
|
||||||
|
else if (e ~ /^\[/) scope = "pinned"
|
||||||
|
else scope = "unbound"
|
||||||
|
target = e
|
||||||
|
sub(/\/[a-z]+$/, "", target)
|
||||||
|
n = split(target, parts, ":")
|
||||||
|
target = parts[n]
|
||||||
|
if (target ~ /^[0-9]+(-[0-9]+)?$/) printf "%s|%s|%s\n", svc, target, scope
|
||||||
|
}
|
||||||
|
/^services:[ \t]*$/ { insvc = 1; svcind = -1; next }
|
||||||
|
!insvc { next }
|
||||||
|
# a service is the first level of keys under services:
|
||||||
|
/^ *[a-zA-Z0-9_.-]+:[ \t]*$/ && (svcind == -1 || indent($0) == svcind) {
|
||||||
|
if (svcind == -1) svcind = indent($0)
|
||||||
|
svc = $1; sub(/:$/, "", svc); inports = 0; next
|
||||||
|
}
|
||||||
|
/^ *ports:/ { inports = 1; portind = indent($0); next }
|
||||||
|
# the list items of a ports: block, whatever indent they use
|
||||||
|
inports && /^ *- / && indent($0) >= portind { emit($0); next }
|
||||||
|
inports && /^ *[a-zA-Z0-9_.-]+:/ { inports = 0 }
|
||||||
|
' "$@"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ See `references/conventions.md`.
|
|||||||
- Check what a stack opens before starting it on a server that faces the
|
- 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,
|
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
|
and on linux the host firewall does not see it, because docker writes its own
|
||||||
rules. Bind it instead: `ports: ["${MYOS_BIND_PRIVATE}::<port>"]`.
|
rules. A port reported as `unbound` was published without anyone choosing an
|
||||||
|
address: bind it with `ports: ["${MYOS_BIND_PRIVATE}::<port>"]`.
|
||||||
- Never run `myos clean` on a host stack: it removes images **and volumes**,
|
- Never run `myos clean` on a host stack: it removes images **and volumes**,
|
||||||
including the certificates.
|
including the certificates.
|
||||||
- Secrets belong in a file outside the repository, never in a compose file.
|
- Secrets belong in a file outside the repository, never in a compose file.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ myos [options] <command> [stack...] [VAR=value...] [-- args...]
|
|||||||
| `env [VAR...]` | resolved variables |
|
| `env [VAR...]` | resolved variables |
|
||||||
| `env-update` | fill the `.env` from the `.env.dist` templates |
|
| `env-update` | fill the `.env` from the `.env.dist` templates |
|
||||||
| `expose [--strict]` | what the stacks publish, and to whom |
|
| `expose [--strict]` | what the stacks publish, and to whom |
|
||||||
|
| `cert list\|issue\|renew\|show` | certificates, derived from the route tags |
|
||||||
| `export [--make]` | every setting of the stacks, as `KEY=value` |
|
| `export [--make]` | every setting of the stacks, as `KEY=value` |
|
||||||
| `doctor` | check the installation |
|
| `doctor` | check the installation |
|
||||||
| `version` | the myos version |
|
| `version` | the myos version |
|
||||||
|
|||||||
@@ -198,22 +198,61 @@ services:
|
|||||||
addresses; `MYOS_MESH_IFACE` names the interface when it is not one of
|
addresses; `MYOS_MESH_IFACE` names the interface when it is not one of
|
||||||
easytier, tun0, tailscale0, mycelium or wg0.
|
easytier, tun0, tailscale0, mycelium or wg0.
|
||||||
|
|
||||||
A stack also declares what it means, so an audit can tell a deliberate choice
|
There is nothing else to declare: the scope **is** the binding the file asks
|
||||||
from an oversight:
|
for. A port written `- 80` or `- "9000:9000"` is *unbound*, which means docker
|
||||||
|
opens it on every address and nobody chose that.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
<PREFIX>_SERVICE_EXPOSE=public # the whole stack
|
myos expose # what each stack publishes, on which address
|
||||||
<PREFIX>_SERVICE_443_EXPOSE=public # one port
|
myos expose --strict # exits 1 when a port is published without a binding
|
||||||
```
|
```
|
||||||
|
|
||||||
`<PREFIX>` is `HOST_<name>` for a host stack, `USER_<name>` for a user stack,
|
The command reads the compose files as written **and** the resolved
|
||||||
`<name>` otherwise.
|
configuration, and shows both: the binding the stack asked for, and the address
|
||||||
|
it ends up on. Resolving first would lose the difference, since
|
||||||
|
`${MYOS_BIND_PRIVATE}` and a hand-written `127.0.0.1` both become `127.0.0.1`,
|
||||||
|
and an unbound port becomes `0.0.0.0` exactly like a deliberate public one.
|
||||||
|
|
||||||
|
The split of responsibility: the **scope** belongs to the stack, in its compose
|
||||||
|
file; the **address** of a scope belongs to the host, in its configuration.
|
||||||
|
|
||||||
|
## Certificates
|
||||||
|
|
||||||
|
A site gets a certificate by being routed, not by being written down a second
|
||||||
|
time. `myos cert` reads the same `urlprefix-` tags fabio routes on, and decides
|
||||||
|
what to ask for:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
myos expose # what each stack publishes, and its declared scope
|
myos cert list # what would be asked for, and over which challenge
|
||||||
myos expose --strict # exits 1 when a port faces the world undeclared
|
myos cert issue # ask for it
|
||||||
|
myos cert renew # what is close to expiry, for a cron
|
||||||
|
myos cert show # what exists, and when it expires
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| a tag routes | myos asks for | challenge |
|
||||||
|
|---|---|---|
|
||||||
|
| `app.example.org` | a certificate for that name | http-01 |
|
||||||
|
| `*.ipns.example.org` | `ipns.example.org` **and** `*.ipns.example.org` | dns-01 |
|
||||||
|
|
||||||
|
That is the whole of "per site or wildcard as needed": a wildcard is asked for
|
||||||
|
where a tag uses one, and it absorbs the concrete names it covers. A wildcard
|
||||||
|
covers one label, so `*.example.org` absorbs `a.example.org` but not
|
||||||
|
`a.b.example.org`, which keeps its own certificate.
|
||||||
|
|
||||||
|
`MYOS_CERT_MODE=per-site` never asks for a wildcard, which keeps everything on
|
||||||
|
http-01 and needs no DNS credentials. `wildcard` asks for one per domain.
|
||||||
|
|
||||||
|
The issuer is [dehydrated](https://github.com/dehydrated-io/dehydrated), a
|
||||||
|
shell script, in the `host/dehydrated` stack. It answers http-01 itself on a
|
||||||
|
port bound to the loopback, which fabio routes
|
||||||
|
`/.well-known/acme-challenge/` to. A wildcard needs dns-01, so point
|
||||||
|
`HOST_DEHYDRATED_DNS_HOOK` at your provider's script; it receives dehydrated's
|
||||||
|
own hook arguments.
|
||||||
|
|
||||||
|
Certificates land where fabio looks for them, `<name>-cert.pem` and
|
||||||
|
`<name>-key.pem` under `/host/certs`, written to a temporary name and moved, so
|
||||||
|
fabio never reads half a file.
|
||||||
|
|
||||||
## Groups
|
## Groups
|
||||||
|
|
||||||
A group is a lowercase name whose value lists stacks. It can live in a `.env`,
|
A group is a lowercase name whose value lists stacks. It can live in a `.env`,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Benchmark of the engines
|
||||||
|
|
||||||
|
Same work, four engines, five runs, median. Docker is the mock of
|
||||||
|
`spec/support/bin`, the catalogue is `myos-stacks` reached through
|
||||||
|
`$HOME/.local/share/myos/stack`, the environment is `env.sh`.
|
||||||
|
|
||||||
|
sh spec/bench/run.sh
|
||||||
|
|
||||||
|
`justfile` is a prototype of just as the engine: shebang recipes that source
|
||||||
|
`lib/*.sh` once. `go/main.go` is a prototype of the core in Go: stack path,
|
||||||
|
groups, compose files, project name, dry-run command; `export` runs one `sh`
|
||||||
|
per stack directory to evaluate the shell hooks.
|
||||||
|
|
||||||
|
## Results, 2026-09-05, Mac Studio M2 Ultra
|
||||||
|
|
||||||
|
| work | make | sh (bin/myos) | just | go |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| fixed cost, empty target | 312 ms | 50 ms | 170 ms | 23 ms |
|
||||||
|
| `up` 1 / 3 stacks, no hooks in the stack | 792 / 2201 | 177 / 332 | 204 / 268 | 23 / 23 |
|
||||||
|
| `up` 1 / 3 stacks, real catalogue with hooks | — | 353 / 855 | (prototype does not load hooks) | (idem) |
|
||||||
|
| `export`, 80 settings of the `host` group | — | 1477 | 1314 | 720 (1 sh) |
|
||||||
|
| same, `MYOS_VAR_MEMO=1` | — | 1503 | 1318 | 774 |
|
||||||
|
| one computed setting (`HOST_FABIO_SERVICE_9998_TAGS`) | — | ~43 ms net (69 − 26) | | |
|
||||||
|
|
||||||
|
Reference points: `sh -c :` 24 ms, `just --version` 27 ms, sourcing `lib/*.sh` +2 ms.
|
||||||
|
|
||||||
|
## What it says
|
||||||
|
|
||||||
|
- The engine's own cost: go flat at 23 ms; sh 177 ms + ~63 ms per stack; just
|
||||||
|
204 ms + ~32 ms per stack; make 792 ms + ~700 ms per stack (it re-reads
|
||||||
|
itself for every stack).
|
||||||
|
- The shell hooks cost ~40 ms per computed setting, on every engine: 80
|
||||||
|
settings ≈ 0.7 s even from Go, which runs the very same `sh`. Memoisation
|
||||||
|
changes nothing, because the cost is not repeated lookups: each `tagprefix`
|
||||||
|
spawns 15-20 command substitutions for distinct, mostly empty, variables.
|
||||||
|
- `bin/myos` doubles that to 1.5 s by loading the hooks of a directory once
|
||||||
|
per stack reference instead of once per directory: `host/consul`,
|
||||||
|
`host/fabio` and `host/registrator` share `stack/host/_stack.sh`.
|
||||||
|
- just's fixed cost (170 ms for a shebang recipe, against 27 ms for `just
|
||||||
|
--version`) is its own overhead of writing and running the recipe script.
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# bench.sh LABEL N -- CMD... run CMD N times, print the median wall time in ms
|
||||||
|
# Hermetic: docker is the mock of spec/support/bin, config comes from the
|
||||||
|
# environment only, HOME points at the fixture catalogue.
|
||||||
|
set -u
|
||||||
|
label=$1; n=$2; shift 2; [ "$1" = "--" ] && shift
|
||||||
|
i=0; times=""
|
||||||
|
while [ "$i" -lt "$n" ]; do
|
||||||
|
s=$(python3 -c 'import time;print(int(time.time()*1e6))')
|
||||||
|
"$@" >/dev/null 2>&1
|
||||||
|
e=$(python3 -c 'import time;print(int(time.time()*1e6))')
|
||||||
|
times="$times $(( (e - s) / 1000 ))"
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
median=$(printf '%s\n' $times | sort -n | awk '{a[NR]=$1} END {print a[int((NR+1)/2)]}')
|
||||||
|
printf '%-44s %6s ms (runs:%s)\n' "$label" "$median" "$times"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# the hermetic environment every engine runs in
|
||||||
|
export PATH=/Users/aya/dev/myos/spec/support/bin:/Users/aya/.local/bin:/usr/bin:/bin
|
||||||
|
export HOME=/tmp/myos-bench/home WORKDIR=/tmp/myos-bench/wd MYOS_ROOT=/Users/aya/dev/myos
|
||||||
|
export USER=tester HOSTNAME=testhost DOMAIN=example.test ENV=local DRYRUN=true
|
||||||
|
export MYOS_CONF=/dev/null MYOS_PROJECT_FORMAT=user-app-env DOCKER_MACHINE=x86_64 DOCKER_SYSTEM=Linux
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
// A prototype of the myos core in Go, just large enough to be benchmarked
|
||||||
|
// fairly against the other engines: stack path, group expansion, compose file
|
||||||
|
// resolution across every directory of the path, project name, and the
|
||||||
|
// dry-run compose command. Same rules as lib/stack.sh and lib/naming.sh.
|
||||||
|
//
|
||||||
|
// export delegates the shell hooks to ONE sh per stack directory, which is
|
||||||
|
// what a Go engine would do to keep the developer contract in shell.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func env(k, def string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// stackPath: the directories stacks are looked up in, project first
|
||||||
|
func stackPath(workdir string) []string {
|
||||||
|
home := env("HOME", "/nonexistent")
|
||||||
|
root := env("MYOS_ROOT", ".")
|
||||||
|
prefix := filepath.Dir(filepath.Dir(root))
|
||||||
|
var out []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, d := range []string{workdir, filepath.Join(workdir, ".."), filepath.Join(home, ".local/share"), filepath.Join(prefix, "share"), "/usr/local/share", "/usr/share"} {
|
||||||
|
for _, c := range []string{filepath.Join(d, "stack"), filepath.Join(d, "myos/stack")} {
|
||||||
|
if st, err := os.Stat(c); err == nil && st.IsDir() {
|
||||||
|
if r, err := filepath.EvalSymlinks(c); err == nil {
|
||||||
|
c = r
|
||||||
|
}
|
||||||
|
if !seen[c] {
|
||||||
|
seen[c] = true
|
||||||
|
out = append(out, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupValue: the list a lowercase group name expands to, from <g>.env,
|
||||||
|
// <g>/<g>.env or <g>/_stack.env along the path
|
||||||
|
func groupValue(path []string, name string) string {
|
||||||
|
if strings.ContainsAny(name, "/:.") || strings.ToLower(name) != name {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if v := os.Getenv(name); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
for _, d := range path {
|
||||||
|
for _, f := range []string{filepath.Join(d, name+".env"), filepath.Join(d, name, name+".env"), filepath.Join(d, name, "_stack.env")} {
|
||||||
|
b, err := os.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(b), "\n") {
|
||||||
|
if strings.HasPrefix(line, name+"=") {
|
||||||
|
return strings.Trim(strings.TrimPrefix(line, name+"="), "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func expand(path []string, refs []string, depth int) []string {
|
||||||
|
var out []string
|
||||||
|
for _, r := range refs {
|
||||||
|
if v := groupValue(path, r); v != "" && depth < 16 {
|
||||||
|
out = append(out, expand(path, strings.Fields(v), depth+1)...)
|
||||||
|
} else {
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func stackName(ref string) string {
|
||||||
|
r := strings.TrimSuffix(ref, "/")
|
||||||
|
if i := strings.LastIndex(r, ":"); i >= 0 {
|
||||||
|
r = r[:i]
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(filepath.Base(r), ".yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stackDirs: every directory of the path holding the stack, least specific first
|
||||||
|
func stackDirs(path []string, ref string) []string {
|
||||||
|
r := strings.TrimSuffix(ref, "/")
|
||||||
|
if i := strings.LastIndex(r, ":"); i >= 0 {
|
||||||
|
r = r[:i]
|
||||||
|
}
|
||||||
|
name := stackName(ref)
|
||||||
|
var found []string
|
||||||
|
for _, d := range path {
|
||||||
|
var hit string
|
||||||
|
if st, err := os.Stat(filepath.Join(d, r)); err == nil && st.IsDir() {
|
||||||
|
hit = filepath.Join(d, r)
|
||||||
|
} else if _, err := os.Stat(filepath.Join(d, r+".yml")); err == nil {
|
||||||
|
hit = filepath.Dir(filepath.Join(d, r))
|
||||||
|
} else if st, err := os.Stat(filepath.Join(d, name)); err == nil && st.IsDir() {
|
||||||
|
hit = filepath.Join(d, name)
|
||||||
|
}
|
||||||
|
if hit != "" {
|
||||||
|
found = append([]string{hit}, found...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(p string) bool { _, err := os.Stat(p); return err == nil }
|
||||||
|
|
||||||
|
// composeFiles: the files that exist, in the order the framework loads them
|
||||||
|
func composeFiles(dir string, names, suffixes []string, envName string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, e := range []string{"yml", "yaml"} {
|
||||||
|
for _, n := range names {
|
||||||
|
for _, f := range []string{
|
||||||
|
filepath.Join(dir, n+"."+e), filepath.Join(dir, n+"."+envName+"."+e),
|
||||||
|
filepath.Join(dir, envName, n+"."+e), filepath.Join(dir, envName, n+"."+envName+"."+e)} {
|
||||||
|
if exists(f) {
|
||||||
|
out = append(out, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range suffixes {
|
||||||
|
for _, f := range []string{filepath.Join(dir, n+"."+s+"."+e), filepath.Join(dir, n+"."+s+"."+envName+"."+e)} {
|
||||||
|
if exists(f) {
|
||||||
|
out = append(out, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func scope(ref string) string {
|
||||||
|
switch strings.SplitN(ref, "/", 2)[0] {
|
||||||
|
case "host":
|
||||||
|
return "host"
|
||||||
|
case "User", "user":
|
||||||
|
return "user"
|
||||||
|
case "cluster":
|
||||||
|
return "cluster"
|
||||||
|
}
|
||||||
|
return "app"
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectName(sc, user, envName, app string) string {
|
||||||
|
switch sc {
|
||||||
|
case "host":
|
||||||
|
return env("HOST_COMPOSE_PROJECT_NAME", env("HOSTNAME", "localhost"))
|
||||||
|
case "user":
|
||||||
|
return user
|
||||||
|
case "cluster":
|
||||||
|
return strings.ToLower(app)
|
||||||
|
}
|
||||||
|
n := strings.NewReplacer(".", "", "-", "", "_", "").Replace(strings.ToLower(app))
|
||||||
|
if env("MYOS_PROJECT_FORMAT", "user-env-app") == "user-app-env" {
|
||||||
|
return user + "-" + n + "-" + envName
|
||||||
|
}
|
||||||
|
return user + "-" + envName + "-" + n
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: myos-go <noop|up|export> [stack...]")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
workdir := env("WORKDIR", ".")
|
||||||
|
envName := env("ENV", "local")
|
||||||
|
user := env("USER", "tester")
|
||||||
|
path := stackPath(workdir)
|
||||||
|
suffixes := []string{"app", "labels", "networks", "ssh", "volumes", "latest"}
|
||||||
|
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "noop":
|
||||||
|
return
|
||||||
|
case "up":
|
||||||
|
refs := expand(path, os.Args[2:], 0)
|
||||||
|
byProject := map[string][]string{}
|
||||||
|
var order []string
|
||||||
|
for _, ref := range refs {
|
||||||
|
app := stackName(ref)
|
||||||
|
var files []string
|
||||||
|
for _, d := range stackDirs(path, ref) {
|
||||||
|
files = append(files, composeFiles(d, []string{"docker-compose", app}, suffixes, envName)...)
|
||||||
|
}
|
||||||
|
p := projectName(scope(ref), user, envName, app)
|
||||||
|
if _, ok := byProject[p]; !ok {
|
||||||
|
order = append(order, p)
|
||||||
|
}
|
||||||
|
byProject[p] = append(byProject[p], files...)
|
||||||
|
}
|
||||||
|
for _, p := range order {
|
||||||
|
files := append(byProject[p], filepath.Join(env("MYOS_ROOT", "."), "share/compose/networks.yml"))
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("docker compose")
|
||||||
|
for _, f := range files {
|
||||||
|
b.WriteString(" -f " + f)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s -p %s up -d\n", b.String(), p)
|
||||||
|
}
|
||||||
|
case "export":
|
||||||
|
// one sh per stack directory evaluates its hooks and prints every value
|
||||||
|
refs := expand(path, os.Args[2:], 0)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
var dirs []string
|
||||||
|
for _, ref := range refs {
|
||||||
|
for _, d := range stackDirs(path, ref) {
|
||||||
|
if !seen[d] {
|
||||||
|
seen[d] = true
|
||||||
|
dirs = append(dirs, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(dirs)
|
||||||
|
root := env("MYOS_ROOT", ".")
|
||||||
|
for _, d := range dirs {
|
||||||
|
script := fmt.Sprintf(`for m in core str var tags naming stack config compose hooks; do . %s/lib/$m.sh; done
|
||||||
|
[ -f %s/_stack.sh ] || exit 0
|
||||||
|
myos_stack_hooks %s _
|
||||||
|
for v in $(sed -n 's/^myos_default_\([A-Za-z_][A-Za-z0-9_]*\)().*/\1/p' %s/_stack.sh | sort -u); do printf '%%s=%%s\n' "$v" "$(myos_var "$v")"; done`, root, d, d, d)
|
||||||
|
cmd := exec.Command("sh", "-c", script)
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
_ = cmd.Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# just as the engine: the interface is a justfile, the logic stays in lib/*.sh.
|
||||||
|
# Recipes are shebang recipes, so a whole body runs in ONE sh that sources
|
||||||
|
# lib/ once; what is measured is just's own overhead on top of the shell.
|
||||||
|
|
||||||
|
set export
|
||||||
|
|
||||||
|
MYOS_ROOT := env_var_or_default("MYOS_ROOT", "/Users/aya/dev/myos")
|
||||||
|
WORKDIR := env_var_or_default("WORKDIR", justfile_directory())
|
||||||
|
ENV := env_var_or_default("ENV", "local")
|
||||||
|
USER := env_var_or_default("USER", "tester")
|
||||||
|
HOSTNAME := env_var_or_default("HOSTNAME", "testhost")
|
||||||
|
DOMAIN := env_var_or_default("DOMAIN", "example.test")
|
||||||
|
DRYRUN := env_var_or_default("DRYRUN", "true")
|
||||||
|
|
||||||
|
# a recipe that does nothing: the fixed cost of just + one sh + sourcing lib/
|
||||||
|
noop:
|
||||||
|
#!/bin/sh
|
||||||
|
for m in core str var tags naming stack config compose hooks; do . $MYOS_ROOT/lib/$m.sh; done
|
||||||
|
|
||||||
|
# up STACKS: resolve every stack of the groups, group by compose project,
|
||||||
|
# print one compose command per project (what bin/myos does)
|
||||||
|
up +stacks:
|
||||||
|
#!/bin/sh
|
||||||
|
for m in core str var tags naming stack config compose hooks; do . $MYOS_ROOT/lib/$m.sh; done
|
||||||
|
rows=""
|
||||||
|
for ref in $(myos_group_expand {{stacks}}); do
|
||||||
|
files=""
|
||||||
|
for d in $(myos_stack_dirs "$ref"); do
|
||||||
|
files="$files $(myos_compose_files "$d" "docker-compose $(myos_stack_name "$ref")" "$(myos_compose_suffixes)" "$ENV" | tr '\n' ' ')"
|
||||||
|
done
|
||||||
|
app=$(myos_stack_name "$ref")
|
||||||
|
project=$(myos_project_name "$(myos_scope "$ref")" "$USER" "$ENV" "$app")
|
||||||
|
rows="$rows
|
||||||
|
$project|$files"
|
||||||
|
done
|
||||||
|
for project in $(printf '%s\n' "$rows" | sed '/^$/d' | cut -d'|' -f1 | awk '!s[$0]++'); do
|
||||||
|
files=$(printf '%s\n' "$rows" | awk -F'|' -v p="$project" '$1==p {print $2}' | tr ' ' '\n' | sed '/^$/d' | awk '!s[$0]++')
|
||||||
|
fargs=""; for f in $files $MYOS_ROOT/share/compose/networks.yml; do fargs="$fargs -f $f"; done
|
||||||
|
echo "docker compose$fargs -p $project up -d"
|
||||||
|
done
|
||||||
|
|
||||||
|
# export STACKS: every setting the hooks of the stacks declare
|
||||||
|
export +stacks:
|
||||||
|
#!/bin/sh
|
||||||
|
for m in core str var tags naming stack config compose hooks; do . $MYOS_ROOT/lib/$m.sh; done
|
||||||
|
refs=$(myos_group_expand {{stacks}})
|
||||||
|
for ref in $refs; do
|
||||||
|
for d in $(myos_stack_dirs "$ref"); do myos_stack_hooks "$d" "$(myos_stack_name "$ref")"; done
|
||||||
|
done
|
||||||
|
names=$(for ref in $refs; do for d in $(myos_stack_dirs "$ref"); do
|
||||||
|
[ -f "$d/_stack.sh" ] && sed -n 's/^myos_default_\([A-Za-z_][A-Za-z0-9_]*\)().*/\1/p' "$d/_stack.sh"; done; done | sort -u)
|
||||||
|
for v in $names; do printf '%s=%s\n' "$v" "$(myos_var "$v")"; done
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# the full matrix: 5 runs each, median, every engine on the same work
|
||||||
|
. /tmp/myos-bench/env.sh; cd "$WORKDIR"
|
||||||
|
B=/tmp/myos-bench/bench.sh; N=5
|
||||||
|
MK="make -esC $MYOS_ROOT MYOS=. WORKDIR=$WORKDIR"
|
||||||
|
SH="$MYOS_ROOT/bin/myos"
|
||||||
|
JU="just --justfile /tmp/myos-bench/justfile"
|
||||||
|
GO=/tmp/myos-bench/myos-go
|
||||||
|
S1="host/consul"; S2="host/consul host/fabio"; S3="host/consul host/fabio host/registrator"
|
||||||
|
|
||||||
|
echo "== cout fixe : demarrage + cible vide"
|
||||||
|
$B "make noop" $N -- $MK FORCE
|
||||||
|
$B "sh noop (myos version)" $N -- $SH version
|
||||||
|
$B "just noop (parse + 1 sh + source lib/)" $N -- $JU noop
|
||||||
|
$B "go noop" $N -- $GO noop
|
||||||
|
echo
|
||||||
|
echo "== up : 1 / 2 / 3 stacks, dry-run"
|
||||||
|
$B "make up 1" $N -- $MK up STACK="$S1"
|
||||||
|
$B "make up 2" $N -- $MK up STACK="$S2"
|
||||||
|
$B "make up 3" $N -- $MK up STACK="$S3"
|
||||||
|
$B "sh up 1" $N -- $SH up host/consul
|
||||||
|
$B "sh up 2" $N -- $SH up host/consul host/fabio
|
||||||
|
$B "sh up 3" $N -- $SH up host/consul host/fabio host/registrator
|
||||||
|
$B "just up 1" $N -- $JU up host/consul
|
||||||
|
$B "just up 2" $N -- $JU up host/consul host/fabio
|
||||||
|
$B "just up 3" $N -- $JU up host/consul host/fabio host/registrator
|
||||||
|
$B "go up 1" $N -- $GO up host/consul
|
||||||
|
$B "go up 2" $N -- $GO up host/consul host/fabio
|
||||||
|
$B "go up 3" $N -- $GO up host/consul host/fabio host/registrator
|
||||||
|
echo
|
||||||
|
echo "== export : les 80 reglages du groupe host (evaluation des hooks shell)"
|
||||||
|
$B "sh export, hooks tels quels" $N -- $SH export STACK=host
|
||||||
|
$B "just export, hooks tels quels" $N -- $JU export host
|
||||||
|
$B "go export, hooks tels quels (1 sh/repertoire)" $N -- $GO export host
|
||||||
|
echo
|
||||||
|
echo "== export : memes hooks, evalues en une passe (MYOS_VAR_MEMO=1)"
|
||||||
|
MYOS_VAR_MEMO=1 $B "sh export, memoise" $N -- $SH export STACK=host
|
||||||
|
MYOS_VAR_MEMO=1 $B "just export, memoise" $N -- $JU export host
|
||||||
|
MYOS_VAR_MEMO=1 $B "go export, memoise" $N -- $GO export host
|
||||||
@@ -91,3 +91,4 @@ cmd-recreate | host-project | recreate host/consul
|
|||||||
cmd-status | host-project | status host/consul
|
cmd-status | host-project | status host/consul
|
||||||
expose-none | host-project | expose host/consul
|
expose-none | host-project | expose host/consul
|
||||||
expose-default | app-git | expose
|
expose-default | app-git | expose
|
||||||
|
cert-none | host-project | cert list host/consul
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
no routed hostname: nothing to certify
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
WARNING: myos[0] cert-rule-exists: target cert unavailable in app myos
|
||||||
|
WARNING: myos[0] list-rule-exists: target list unavailable in app myos
|
||||||
|
WARNING: myos[0] host/consul-rule-exists: target host/consul unavailable in app myos
|
||||||
|
[exit 0]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#shellcheck shell=sh
|
||||||
|
MYOS_CERT_MODE=$1
|
||||||
|
myos_cert_names() { printf 'urlprefix-*.ipns.ex.org/*\nurlprefix-a.ex.org/*\n' | myos_cert_parse; }
|
||||||
|
myos_cert_needs_dns && echo yes || echo no
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#shellcheck shell=sh
|
||||||
|
# myos_cert_names normally reads the compose configuration; here it is replaced
|
||||||
|
# by a fixed list so the grouping can be checked on its own.
|
||||||
|
MYOS_CERT_MODE=$1
|
||||||
|
_fixture=${2-'urlprefix-ipfs.ex.org/*
|
||||||
|
urlprefix-*.ipns.ex.org/*
|
||||||
|
urlprefix-a.ipns.ex.org/*
|
||||||
|
urlprefix-ex.org/*'}
|
||||||
|
myos_cert_names() { printf '%s' "$_fixture" | myos_cert_parse; }
|
||||||
|
myos_cert_groups
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#shellcheck shell=sh
|
||||||
|
printf 'urlprefix-ipfs.ex.org/api/*\nurlprefix-*.ipns.ex.org/*\nurlprefix-ex.org:443/* proto=https\nurlprefix-*/*\n' | myos_cert_parse
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#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/cert.sh
|
||||||
|
|
||||||
|
# The hostnames a server must certify are already declared in the fabio route
|
||||||
|
# tags. Reading them there rather than in a domains.txt keeps one source of
|
||||||
|
# truth, and decides on its own which name needs a wildcard.
|
||||||
|
Describe 'lib/cert.sh'
|
||||||
|
Describe 'myos_cert_parse'
|
||||||
|
one_tag() { printf 'urlprefix-a.ex.org/x/*\n' | myos_cert_parse; }
|
||||||
|
It 'reads a hostname out of a route tag'
|
||||||
|
When call one_tag
|
||||||
|
The output should equal "a.ex.org"
|
||||||
|
End
|
||||||
|
It 'drops the path, the port and the options'
|
||||||
|
When run source spec/unit/cert_parse_helper.sh
|
||||||
|
The line 1 should equal "*.ipns.ex.org"
|
||||||
|
The line 2 should equal "ex.org"
|
||||||
|
The line 3 should equal "ipfs.ex.org"
|
||||||
|
The lines of output should equal 3
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Describe 'myos_cert_covers'
|
||||||
|
Parameters
|
||||||
|
"ex.org" "a.ex.org" success
|
||||||
|
"ex.org" "ex.org" failure
|
||||||
|
"ex.org" "a.b.ex.org" failure
|
||||||
|
"ex.org" "other.org" failure
|
||||||
|
End
|
||||||
|
It "*.$1 against $2"
|
||||||
|
When call myos_cert_covers "$1" "$2"
|
||||||
|
The status should be "$3"
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Describe 'myos_cert_groups'
|
||||||
|
It 'asks for a wildcard where a tag uses one, and absorbs what it covers'
|
||||||
|
When run source spec/unit/cert_groups_helper.sh auto
|
||||||
|
The line 1 should equal "ipns.ex.org *.ipns.ex.org"
|
||||||
|
The output should include "ipfs.ex.org"
|
||||||
|
The output should not include "a.ipns.ex.org *"
|
||||||
|
End
|
||||||
|
It 'never asks for a wildcard in per-site mode'
|
||||||
|
When run source spec/unit/cert_groups_helper.sh per-site
|
||||||
|
The output should not include "*"
|
||||||
|
The output should include "a.ipns.ex.org"
|
||||||
|
End
|
||||||
|
It 'asks for one per domain in wildcard mode'
|
||||||
|
When run source spec/unit/cert_groups_helper.sh wildcard
|
||||||
|
The output should include "ex.org *.ex.org"
|
||||||
|
End
|
||||||
|
It 'says nothing when nothing is routed'
|
||||||
|
When run source spec/unit/cert_groups_helper.sh auto ""
|
||||||
|
The output should equal ""
|
||||||
|
End
|
||||||
|
End
|
||||||
|
|
||||||
|
Describe 'myos_cert_needs_dns'
|
||||||
|
It 'is true when a wildcard is asked for'
|
||||||
|
When run source spec/unit/cert_dns_helper.sh auto
|
||||||
|
The output should equal "yes"
|
||||||
|
End
|
||||||
|
It 'is false without one'
|
||||||
|
When run source spec/unit/cert_dns_helper.sh per-site
|
||||||
|
The output should equal "no"
|
||||||
|
End
|
||||||
|
End
|
||||||
|
End
|
||||||
+38
-17
@@ -54,26 +54,47 @@ Describe 'lib/expose.sh'
|
|||||||
End
|
End
|
||||||
End
|
End
|
||||||
|
|
||||||
Describe 'myos_expose_scope'
|
Describe 'myos_expose_declared'
|
||||||
It 'is private unless the stack says otherwise'
|
setup() { MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-exp.XXXXXX"); }
|
||||||
When call myos_expose_scope HOST_FTPS ftps 21
|
cleanup() { rm -rf "$MYOS_TMP"; }
|
||||||
The output should equal "private"
|
BeforeEach setup
|
||||||
|
AfterEach cleanup
|
||||||
|
|
||||||
|
# The scope is not declared on the side: it is which binding the compose
|
||||||
|
# file asks for. Reading the resolved configuration instead would lose the
|
||||||
|
# difference, since every form ends up as a plain address.
|
||||||
|
It 'reads the scope out of the binding each port asks for'
|
||||||
|
printf 'services:\n a:\n ports:\n' > "$MYOS_TMP/c.yml"
|
||||||
|
printf ' - "${MYOS_BIND_PUBLIC}:443:443"\n' >> "$MYOS_TMP/c.yml"
|
||||||
|
printf ' - "${MYOS_BIND_PRIVATE}::8080"\n' >> "$MYOS_TMP/c.yml"
|
||||||
|
printf ' - "${MYOS_BIND_MESH}::7946"\n' >> "$MYOS_TMP/c.yml"
|
||||||
|
printf ' - "127.0.0.1:5432:5432"\n' >> "$MYOS_TMP/c.yml"
|
||||||
|
printf ' - 80\n' >> "$MYOS_TMP/c.yml"
|
||||||
|
When call myos_expose_declared "$MYOS_TMP/c.yml"
|
||||||
|
The line 1 should equal "a|443|public"
|
||||||
|
The line 2 should equal "a|8080|private"
|
||||||
|
The line 3 should equal "a|7946|mesh"
|
||||||
|
The line 4 should equal "a|5432|pinned"
|
||||||
|
The line 5 should equal "a|80|unbound"
|
||||||
End
|
End
|
||||||
It 'reads the scope of one port'
|
|
||||||
HOST_FTPS_SERVICE_21_EXPOSE=public
|
It 'calls a plain host:container mapping unbound, because it is'
|
||||||
When call myos_expose_scope HOST_FTPS ftps 21
|
printf 'services:\n a:\n ports:\n - "9000:9000"\n - 25:25\n' > "$MYOS_TMP/c.yml"
|
||||||
The output should equal "public"
|
When call myos_expose_declared "$MYOS_TMP/c.yml"
|
||||||
|
The line 1 should equal "a|9000|unbound"
|
||||||
|
The line 2 should equal "a|25|unbound"
|
||||||
End
|
End
|
||||||
It 'reads the scope of a whole stack'
|
|
||||||
HOST_FTPS_SERVICE_EXPOSE=mesh
|
It 'keeps the protocol out of the port'
|
||||||
When call myos_expose_scope HOST_FTPS ftps 21
|
printf 'services:\n a:\n ports:\n - 4001/udp\n' > "$MYOS_TMP/c.yml"
|
||||||
The output should equal "mesh"
|
When call myos_expose_declared "$MYOS_TMP/c.yml"
|
||||||
|
The output should equal "a|4001|unbound"
|
||||||
End
|
End
|
||||||
It 'prefers the port over the stack'
|
|
||||||
HOST_FTPS_SERVICE_EXPOSE=mesh
|
It 'reports nothing for a service that publishes nothing'
|
||||||
HOST_FTPS_SERVICE_21_EXPOSE=public
|
printf 'services:\n a:\n image: alpine\n' > "$MYOS_TMP/c.yml"
|
||||||
When call myos_expose_scope HOST_FTPS ftps 21
|
When call myos_expose_declared "$MYOS_TMP/c.yml"
|
||||||
The output should equal "public"
|
The output should equal ""
|
||||||
End
|
End
|
||||||
End
|
End
|
||||||
End
|
End
|
||||||
|
|||||||
Reference in New Issue
Block a user