add stack resolution: search path, references, overlays, groups
Replaces the 500-character stack_path expression of make/apps/def.docker.mk:159 and its leaked $(stackz) loop variable with three readable functions, covered by 20 assertions.
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
#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=
|
||||
for _d in "$_wd" "$_wd/.." "$HOME/.local/share" /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_resolve REF print the directory holding the stack, or fail with
|
||||
# MYOS_E_NOSTACK. Sets MYOS_STACK_NAME and MYOS_STACK_VERSION as a side effect.
|
||||
myos_stack_resolve() {
|
||||
_ref=${1%/}
|
||||
# MYOS_STACK_NAME and MYOS_STACK_VERSION are read back by the callers
|
||||
# shellcheck disable=SC2034
|
||||
{
|
||||
MYOS_STACK_VERSION=latest
|
||||
case $_ref in *:*) MYOS_STACK_VERSION=${_ref##*:}; _ref=${_ref%:*} ;; esac
|
||||
MYOS_STACK_NAME=$(basename "$_ref" .yml)
|
||||
}
|
||||
|
||||
# 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/$MYOS_STACK_NAME" ]; then printf '%s' "$_d/$MYOS_STACK_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
|
||||
myos_group_value() {
|
||||
case $1 in .|/*|*/*|*:*) 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
|
||||
[ -f "$_d/$1.env" ] && { _v=$(sed -n "s/^$1=//p" "$_d/$1.env" | tail -1 | tr -d '"'); }
|
||||
[ -z "$_v" ] && [ -f "$_d/$1.mk" ] && { _v=$(myos_mk_group "$_d/$1.mk" "$1"); }
|
||||
[ -z "$_v" ] && [ -f "$_d/$1/$1.mk" ] && { _v=$(myos_mk_group "$_d/$1/$1.mk" "$1"); }
|
||||
[ -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
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
#shellcheck shell=sh
|
||||
Include lib/str.sh
|
||||
Include lib/core.sh
|
||||
Include lib/tags.sh
|
||||
Include lib/stack.sh
|
||||
|
||||
Describe 'lib/stack.sh'
|
||||
setup() {
|
||||
MYOS_SANDBOX=$(myos_sandbox host-project)
|
||||
WORKDIR=$MYOS_SANDBOX/wd
|
||||
HOME=$MYOS_SANDBOX/home
|
||||
MYOS_PATH=
|
||||
ENV=local
|
||||
unset COMPOSE_FILE_WWW COMPOSE_FILE_DNS 2>/dev/null || true
|
||||
}
|
||||
cleanup() { rm -rf "$MYOS_SANDBOX"; }
|
||||
BeforeEach setup
|
||||
AfterEach cleanup
|
||||
|
||||
Describe 'myos_path'
|
||||
It 'puts the project stacks before the shared catalogue'
|
||||
When call myos_path
|
||||
The output should equal "$WORKDIR/stack:$HOME/.local/share/myos/stack"
|
||||
End
|
||||
It 'is overridable'
|
||||
MYOS_PATH=/opt/stacks
|
||||
When call myos_path
|
||||
The output should equal "/opt/stacks"
|
||||
End
|
||||
End
|
||||
|
||||
Describe 'myos_stack_resolve'
|
||||
It 'resolves a stack of the project'
|
||||
When call myos_stack_resolve host/consul
|
||||
The output should equal "$WORKDIR/stack/host"
|
||||
End
|
||||
It 'resolves a stack of the shared catalogue'
|
||||
When call myos_stack_resolve postgres
|
||||
The output should equal "$HOME/.local/share/myos/stack/postgres"
|
||||
End
|
||||
It 'resolves a versioned reference'
|
||||
When run source spec/unit/stack_version_helper.sh
|
||||
The output should equal "postgres 9.6"
|
||||
End
|
||||
It 'resolves a directory reference'
|
||||
When call myos_stack_resolve "$WORKDIR/stack/host"
|
||||
The output should equal "$WORKDIR/stack/host"
|
||||
End
|
||||
It 'fails loudly on an unknown stack'
|
||||
When run myos_stack_resolve nosuchstack
|
||||
The status should equal 3
|
||||
The stderr should include "stack not found: nosuchstack"
|
||||
End
|
||||
End
|
||||
|
||||
Describe 'myos_compose_files'
|
||||
It 'loads the stack file and its env overlay'
|
||||
When call myos_compose_files "$HOME/.local/share/myos/stack/postgres" "docker-compose postgres" "" local
|
||||
The line 1 should equal "$HOME/.local/share/myos/stack/postgres/postgres.yml"
|
||||
The line 2 should equal "$HOME/.local/share/myos/stack/postgres/postgres.local.yml"
|
||||
The lines of output should equal 2
|
||||
End
|
||||
It 'skips the env overlay of another env'
|
||||
When call myos_compose_files "$HOME/.local/share/myos/stack/postgres" "docker-compose postgres" "" master
|
||||
The output should equal "$HOME/.local/share/myos/stack/postgres/postgres.yml"
|
||||
End
|
||||
It 'loads the version overlay as a suffix'
|
||||
When call myos_compose_files "$HOME/.local/share/myos/stack/postgres" "docker-compose postgres" "9.6" master
|
||||
The line 2 should equal "$HOME/.local/share/myos/stack/postgres/postgres.9.6.yml"
|
||||
End
|
||||
It 'loads the suffix overlays in order'
|
||||
When call myos_compose_files "$HOME/.local/share/myos/stack/host" "docker-compose nginx" "www dns" master
|
||||
The line 1 should equal "$HOME/.local/share/myos/stack/host/nginx.yml"
|
||||
The line 2 should equal "$HOME/.local/share/myos/stack/host/nginx.www.yml"
|
||||
The line 3 should equal "$HOME/.local/share/myos/stack/host/nginx.dns.yml"
|
||||
End
|
||||
It 'is empty for a directory without compose files'
|
||||
When call myos_compose_files "$WORKDIR" "docker-compose nothing" "" local
|
||||
The output should equal ""
|
||||
End
|
||||
End
|
||||
|
||||
Describe 'myos_compose_suffixes'
|
||||
It 'keeps the enabled suffixes and drops the false ones'
|
||||
COMPOSE_FILE_LABELS=true; COMPOSE_FILE_WWW=false; COMPOSE_FILE_DNS=true
|
||||
When call myos_compose_suffixes
|
||||
The output should include "labels"
|
||||
The output should include "dns"
|
||||
The output should not include "www"
|
||||
End
|
||||
It 'adds <suffix>.<value> when the value is not a boolean'
|
||||
COMPOSE_FILE_WWW=nginx
|
||||
When call myos_compose_suffixes
|
||||
The output should include "www"
|
||||
The output should include "www.nginx"
|
||||
End
|
||||
End
|
||||
|
||||
Describe 'myos_group_expand'
|
||||
It 'expands a group defined in the environment'
|
||||
host="host/consul host/fabio"
|
||||
When call myos_group_expand host
|
||||
The line 1 should equal "host/consul"
|
||||
The line 2 should equal "host/fabio"
|
||||
End
|
||||
It 'expands a group defined in a legacy .mk of the project'
|
||||
When call myos_group_expand host
|
||||
The line 1 should equal "host/consul"
|
||||
The line 3 should equal "host/registrator"
|
||||
The lines of output should equal 3
|
||||
End
|
||||
It 'expands a group of the shared catalogue'
|
||||
When call myos_group_expand testing
|
||||
The line 1 should equal "drone/drone"
|
||||
The line 2 should equal "redis"
|
||||
End
|
||||
It 'expands recursively'
|
||||
# shellcheck disable=SC2034
|
||||
all="testing extra"; extra="redis"
|
||||
When call myos_group_expand all
|
||||
The lines of output should equal 3
|
||||
End
|
||||
It 'leaves a plain stack reference alone'
|
||||
When call myos_group_expand postgres host/fabio
|
||||
The line 1 should equal "postgres"
|
||||
The line 2 should equal "host/fabio"
|
||||
End
|
||||
It 'refuses an endless group loop'
|
||||
# shellcheck disable=SC2034
|
||||
loop="loop"
|
||||
When run myos_group_expand loop
|
||||
The status should equal 2
|
||||
The stderr should include "nested too deep"
|
||||
End
|
||||
End
|
||||
End
|
||||
@@ -0,0 +1,5 @@
|
||||
#shellcheck shell=sh
|
||||
# Helper: myos_stack_resolve exports the parsed name and version as a side
|
||||
# effect, which a subshell would discard.
|
||||
myos_stack_resolve "postgres:9.6" >/dev/null
|
||||
printf '%s %s\n' "$MYOS_STACK_NAME" "$MYOS_STACK_VERSION"
|
||||
Reference in New Issue
Block a user