Files
myos/lib/stack.sh
T
Yann Autissier 084a25c627 make the hooks usable by the whole catalogue
- hooks load the _stack files of every directory between the stack path root
  and the stack, outermost first: make included both $(dir)/*.mk and
  $(dir)/*/*.mk, so a stack in a subdirectory saw its parent's settings
- a group may be declared in <group>/<group>.env, where the stack lives
- MYOS_STACK_DIR lets a hook read a file it ships next to itself
- myos_filter no longer confuses a literal * in a make pattern with a wildcard,
  and the list helpers no longer let the shell expand a * into filenames
- MACHINE, SYSTEM, HOST and DOMAINNAME join the framework variables a hook sees
- the make shim gains $(call myos-var,NAME), so a .mk target can read a
  setting that now lives in a hook, and it picks up the .mk of every stack
  directory rather than only the project's
2026-09-03 22:30:01 +02:00

191 lines
6.6 KiB
Bash

#shellcheck shell=sh
# stack: where stacks live, how a reference resolves to compose files.
#
# A stack is a directory holding compose files. A reference is
# [<group>/]<name>[:<version>] resolved along MYOS_PATH
# ./ or /abs/path or rel/path the directory itself
# Ported from make/def.docker.mk (STACK_DIR/SHARE_DIR) and
# make/apps/def.docker.mk (compose-file, docker-stack, docker-stack-update).
# myos_path the stack search path, colon separated, existing directories only.
# Project first, then the shared catalogues: a project always wins over the
# catalogue installed system wide.
myos_path() {
[ -n "${MYOS_PATH:-}" ] && { printf '%s' "$MYOS_PATH"; return 0; }
_wd=${WORKDIR:-$PWD}
_name=${STACK_DIR_NAME:-stack}
_out=
# <prefix>/share comes from MYOS_ROOT, so an installation under any prefix
# finds the catalogue installed beside it
_prefix=
[ -n "${MYOS_ROOT:-}" ] && _prefix=$(dirname "$(dirname "$MYOS_ROOT")")/share
for _d in "$_wd" "$_wd/.." "${HOME:-/nonexistent}/.local/share" \
${_prefix:+"$_prefix"} /usr/local/share /usr/share; do
for _c in "$_d/$_name" "$_d/myos/$_name"; do
[ -d "$_c" ] || continue
_c=$(cd "$_c" && pwd -P)
case ":$_out:" in *":$_c:"*) continue ;; esac
_out="${_out:+$_out:}$_c"
done
done
printf '%s' "$_out"
}
# myos_stack_name REF the stack name: "host/fabio:1.6" -> "fabio"
# myos_stack_version REF the version, "latest" when the reference has none
# Both are pure, so a caller can use them inside a command substitution.
myos_stack_name() {
_r=${1%/}
case $_r in *:*) _r=${_r%:*} ;; esac
basename "$_r" .yml
}
myos_stack_version() {
_r=${1%/}
case $_r in *:*) printf '%s' "${_r##*:}" ;; *) printf 'latest' ;; esac
}
# myos_stack_dirs REF every directory of MYOS_PATH holding this stack, least
# specific first. A compose overlay wins over the ones before it, so a project
# that ships stack/postgres/postgres.local.yml refines the postgres stack of
# the catalogue instead of replacing it.
myos_stack_dirs() {
_ref=${1%/}
case $_ref in *:*) _ref=${_ref%:*} ;; esac
_name=$(myos_stack_name "$1")
case $_ref in
.|./*|/*|../*)
[ -d "$_ref" ] && (cd "$_ref" && pwd -P)
return 0 ;;
esac
_found=
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
_hit=
if [ -d "$_d/$_ref" ]; then _hit=$_d/$_ref
elif [ -f "$_d/$_ref.yml" ] || [ -f "$_d/$_ref.yaml" ]; then _hit=$(dirname "$_d/$_ref")
elif [ -d "$_d/$_name" ]; then _hit=$_d/$_name
fi
# least specific last here, then reversed below
[ -n "$_hit" ] && _found="$_hit
$_found"
IFS=:
done
IFS=$_IFS
printf '%s' "$_found" | sed '/^$/d'
}
# myos_stack_resolve REF print the directory holding the stack,
# or fail with MYOS_E_NOSTACK
myos_stack_resolve() {
_ref=${1%/}
case $_ref in *:*) _ref=${_ref%:*} ;; esac
_name=$(myos_stack_name "$1")
# a path reference resolves to itself
case $_ref in
.|./*|/*|../*)
if [ -d "$_ref" ]; then printf '%s' "$(cd "$_ref" && pwd -P)"; return 0; fi
myos_error "no such directory: $_ref"; return "$MYOS_E_NOSTACK" ;;
esac
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
if [ -d "$_d/$_ref" ]; then printf '%s' "$_d/$_ref"; return 0; fi
if [ -f "$_d/$_ref.yml" ] || [ -f "$_d/$_ref.yaml" ]; then
printf '%s' "$(dirname "$_d/$_ref")"; return 0
fi
if [ -d "$_d/$_name" ]; then printf '%s' "$_d/$_name"; return 0; fi
IFS=:
done
IFS=$_IFS
myos_error "stack not found: $1 (searched $(myos_path))"
return "$MYOS_E_NOSTACK"
}
# myos_compose_suffixes the overlay suffixes, from the COMPOSE_FILE_* variables
# that are not false (make: COMPOSE_FILE_SUFFIX). A value other than true also
# yields "<suffix>.<value>", which is how COMPOSE_FILE_WWW=nginx works.
myos_compose_suffixes() {
_out=
for _v in $(set | sed -n 's/^\(COMPOSE_FILE_[A-Z0-9_]*\)=.*/\1/p' | sort -u); do
case $_v in COMPOSE_FILE_SUFFIX) continue ;; esac
_val=$(myos_var "$_v")
case $_val in false|False|FALSE|'') continue ;; esac
_s=$(myos_lower "${_v#COMPOSE_FILE_}")
_out="${_out:+$_out }$_s"
case $_val in true|True|TRUE) ;; *) for _x in $_val; do _out="$_out $_s.$_x"; done ;; esac
done
printf '%s' "$_out"
}
# myos_compose_files DIR NAMES SUFFIXES [ENV]
# Print the compose files that exist, in the order the framework loads them.
myos_compose_files() {
_dir=$1; _names=$2; _suffixes=${3:-}; _env=${4:-${ENV:-local}}
for _e in yml yaml; do
for _n in $_names; do
for _f in \
"$_dir/$_n.$_e" "$_dir/$_n.$_env.$_e" \
"$_dir/$_env/$_n.$_e" "$_dir/$_env/$_n.$_env.$_e"; do
[ -f "$_f" ] && printf '%s\n' "$_f"
done
for _s in $_suffixes; do
for _f in "$_dir/$_n.$_s.$_e" "$_dir/$_n.$_s.$_env.$_e"; do
[ -f "$_f" ] && printf '%s\n' "$_f"
done
done
done
done
return 0
}
# myos_group_expand REF... expand group references recursively.
# A group is a variable whose name is the reference: `host=host/consul host/fabio`
# in the environment, in a .env, in <path>/<group>.env or in a legacy <group>.mk.
myos_group_expand() {
_depth=${MYOS_GROUP_DEPTH:-0}
[ "$_depth" -gt 16 ] && myos_die "$MYOS_E_USAGE" "stack group nested too deep: $*"
for _ref in "$@"; do
_val=$(myos_group_value "$_ref")
if [ -n "$_val" ]; then
# shellcheck disable=SC2086 # the group value is a list of references
MYOS_GROUP_DEPTH=$((_depth + 1)) myos_group_expand $_val
else
printf '%s\n' "$_ref"
fi
done
}
# myos_group_value REF the list a group expands to, empty when not a group.
# Groups are lowercase by convention (host, testing, coroot, default): without
# that rule any environment variable sharing a stack name would be expanded,
# which is how the make engine behaved. The character class is spelled out
# because a-z also matches uppercase under a dictionary collation (fr_FR).
myos_group_value() {
case $1 in .|/*|*/*|*:*) return 0 ;; esac
case $1 in *[![:lower:][:digit:]_-]*) return 0 ;; esac
_v=$(myos_var "$1")
[ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
for _f in "$_d/$1.env" "$_d/$1/$1.env" "$_d/$1/_stack.env"; do
[ -z "$_v" ] && [ -f "$_f" ] && _v=$(sed -n "s/^$1=//p" "$_f" | tail -1 | tr -d '"')
done
for _f in "$_d/$1.mk" "$_d/$1/$1.mk"; do
[ -z "$_v" ] && [ -f "$_f" ] && _v=$(myos_mk_group "$_f" "$1")
done
[ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
IFS=:
done
IFS=$_IFS
return 0
}
# myos_mk_group FILE NAME read `name ?= a b c` out of a legacy .mk snippet
myos_mk_group() {
sed -n "s/^$2[[:space:]]*[?:]\{0,1\}=[[:space:]]*//p" "$1" 2>/dev/null | tail -1
}