From 6192d73cbe939def67bc08eef6f76878d936029e Mon Sep 17 00:00:00 2001 From: Yann Autissier Date: Thu, 3 Sep 2026 21:58:22 +0200 Subject: [PATCH] port the make list functions and JWT, and expose the framework variables to hooks lib/str.sh gains firstword, lastword, or, patsubst, filter, filter_out, addprefix, addsuffix and jwt: what the catalogue .mk files are written in. The make JWT macro split on the commas of its payload; this one does not. lib/context.sh registers COMPOSE_PROJECT_NAME, APP, DOCKER_NETWORK_* and the rest as lazy defaults, so a converted .mk keeps reading them as it did. Hooks also load a directory-level _stack.env and _stack.sh, for the .mk files of the catalogue that hold settings for several stacks at once. --- bin/myos | 4 +- lib/cmd/env.sh | 32 --------- lib/context.sh | 63 +++++++++++++++++ lib/hooks.sh | 6 +- lib/str.sh | 74 ++++++++++++++++++++ spec/golden/expected/chain-build-up-logs.txt | 2 +- spec/support/run.sh | 2 + spec/unit/str_spec.sh | 52 ++++++++++++++ 8 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 lib/context.sh diff --git a/bin/myos b/bin/myos index 0d754f8..c813a1f 100755 --- a/bin/myos +++ b/bin/myos @@ -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; do +for _m in core str var tags naming stack config compose hooks context; do # shellcheck source=/dev/null . "$MYOS_ROOT/lib/$_m.sh" done @@ -225,6 +225,8 @@ fi # shellcheck disable=SC2086 # a list of references MYOS_STACKS=$(myos_group_expand $MYOS_REFS) +myos_context_defaults + # The uri a stack is served on: the tag helpers build on it, so it has to be # known before the hooks run. MYOS_SCOPE_FIRST=$(for _r in $MYOS_STACKS; do myos_scope "$_r"; break; done) diff --git a/lib/cmd/env.sh b/lib/cmd/env.sh index c9315e1..6240e5a 100644 --- a/lib/cmd/env.sh +++ b/lib/cmd/env.sh @@ -46,35 +46,3 @@ myos_cmd_env() { done } -# myos_all_compose_files every compose file of every requested stack, in order -myos_all_compose_files() { - for _ref in $MYOS_STACKS; do - myos_stack_compose_files "$_ref" 2>/dev/null - done - myos_framework_compose_files - return 0 -} - -# myos_first_app / myos_first_scope / myos_first_project -# describe the first requested stack, which is what the introspection commands -# report when several stacks are asked for at once. -myos_first_app() { - for _ref in $MYOS_STACKS; do - case $_ref in - .|./*|/*|../*) basename "$(myos_stack_resolve "$_ref" 2>/dev/null)" ;; - *) myos_stack_name "$_ref" ;; - esac - return 0 - done -} - -myos_first_scope() { - for _ref in $MYOS_STACKS; do myos_scope "$_ref"; return 0; done -} - -myos_first_project() { - for _ref in $MYOS_STACKS; do - myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$(myos_first_app)" - return 0 - done -} diff --git a/lib/context.sh b/lib/context.sh new file mode 100644 index 0000000..e829417 --- /dev/null +++ b/lib/context.sh @@ -0,0 +1,63 @@ +#shellcheck shell=sh +# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos +# context: what the requested stacks resolve to, and the framework variables a +# stack hook may read. +# +# A hook is written the way a .mk was: it may mention COMPOSE_PROJECT_NAME, +# USER or DOMAIN and expect the framework value. Those are registered as lazy +# defaults, so each is computed when read and an explicit value still wins. + +# myos_all_compose_files every compose file of every requested stack, in order +myos_all_compose_files() { + for _ref in $MYOS_STACKS; do + myos_stack_compose_files "$_ref" 2>/dev/null + done + myos_framework_compose_files + return 0 +} + +# myos_first_app / myos_first_scope / myos_first_project +# describe the first requested stack, which is what the introspection commands +# report when several stacks are asked for at once. +myos_first_app() { + for _ref in $MYOS_STACKS; do + case $_ref in + .|./*|/*|../*) basename "$(myos_stack_resolve "$_ref" 2>/dev/null)" ;; + *) myos_stack_name "$_ref" ;; + esac + return 0 + done +} + +myos_first_scope() { + for _ref in $MYOS_STACKS; do myos_scope "$_ref"; return 0; done +} + +myos_first_project() { + for _ref in $MYOS_STACKS; do + myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$(myos_first_app)" + return 0 + done +} + +# myos_context_defaults register the framework variables as lazy defaults +# shellcheck disable=SC2329 # these are reached through myos_var +myos_context_defaults() { + myos_default_APP() { myos_first_app; } + myos_default_APP_NAME() { myos_name "$(myos_first_app)"; } + myos_default_SCOPE() { myos_first_scope; } + myos_default_COMPOSE_PROJECT_NAME() { myos_first_project; } + myos_default_COMPOSE_SERVICE_NAME() { myos_service_name "$(myos_first_project)"; } + myos_default_DOCKER_REPOSITORY() { printf '%s' "$(myos_first_project)" | tr '_-' '//'; } + myos_default_DOCKER_NETWORK_DEFAULT() { myos_network_default "$(myos_first_project)"; } + myos_default_DOCKER_NETWORK_PRIVATE() { myos_network_private "$USER" "$ENV"; } + myos_default_DOCKER_NETWORK_PUBLIC() { myos_network_public "${HOSTNAME:-}"; } + myos_default_DOCKER_NETWORK() { myos_network_private "$USER" "$ENV"; } + myos_default_DOCKER_IMAGE_TAG() { printf 'latest'; } + myos_default_GIT_USER() { printf '%s' "$USER"; } + myos_default_HOST_COMPOSE_PROJECT_NAME() { printf '%s' "${HOSTNAME:-}"; } + myos_default_HOST_DOCKER_VOLUME() { printf '%s' "${HOSTNAME:-}"; } + myos_default_HOST_DOCKER_REPOSITORY() { printf '%s' "${HOSTNAME:-}" | tr '_-' '//'; } + myos_default_USER_COMPOSE_PROJECT_NAME() { myos_resu "${MAIL:-}" | tr '.' '-'; } + myos_default_RESU() { myos_resu "${MAIL:-}"; } +} diff --git a/lib/hooks.sh b/lib/hooks.sh index 16d0603..8a6999b 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -2,6 +2,8 @@ # hooks: the per-stack settings that used to live in a .mk file. # # A stack may ship, next to its compose files: +# _stack.env settings shared by every stack of the directory +# _stack.sh the same, computed # .env dotenv, for plain values # .env. the same, for one environment # .sh shell, for values that have to be computed (fabio tags, JWTs) @@ -14,10 +16,10 @@ # myos_stack_hooks DIR NAME load the hooks of one stack, most specific last myos_stack_hooks() { _hdir=$1; _hname=$2 - for _h in "$_hdir/$_hname.env" "$_hdir/$_hname.env.$ENV"; do + for _h in "$_hdir/_stack.env" "$_hdir/$_hname.env" "$_hdir/$_hname.env.$ENV"; do [ -f "$_h" ] && myos_dotenv_load "$_h" done - for _h in "$_hdir/$_hname.sh" "$_hdir/$_hname.$ENV.sh"; do + for _h in "$_hdir/_stack.sh" "$_hdir/$_hname.sh" "$_hdir/$_hname.$ENV.sh"; do if [ -f "$_h" ]; then myos_debug "hook $_h" # shellcheck source=/dev/null diff --git a/lib/str.sh b/lib/str.sh index f56da3e..274d0e6 100644 --- a/lib/str.sh +++ b/lib/str.sh @@ -31,3 +31,77 @@ myos_verlt() { [ "$1" = "$2" ] && return 1 myos_verle "$1" "$2" } + +# The make list functions the catalogue uses, on space separated words. + +# myos_firstword LIST / myos_lastword LIST +myos_firstword() { for _w in $1; do printf '%s' "$_w"; return 0; done; } +myos_lastword() { _l=; for _w in $1; do _l=$_w; done; printf '%s' "$_l"; } + +# myos_or A B... the first argument that is not empty +myos_or() { for _a in "$@"; do [ -n "$_a" ] && { printf '%s' "$_a"; return 0; }; done; } + +# myos_patsubst PATTERN REPLACEMENT LIST +# The pattern holds one %, standing for any text; the replacement puts it back. +myos_patsubst() { + _pre=${1%%%*}; _suf=${1#*%} + _rpre=${2%%%*}; _rsuf=${2#*%} + _out= + for _w in $3; do + case $_w in + "$_pre"*"$_suf") + _stem=${_w#"$_pre"}; _stem=${_stem%"$_suf"} + _out="${_out:+$_out }$_rpre$_stem$_rsuf" ;; + *) _out="${_out:+$_out }$_w" ;; + esac + done + printf '%s' "$_out" +} + +# myos_filter PATTERNS LIST / myos_filter_out PATTERNS LIST +# A make pattern uses % where a shell pattern uses *. +myos_filter() { + _pats=$(printf '%s' "$1" | tr '%' '*') + _out= + for _w in $2; do + for _p in $_pats; do + # shellcheck disable=SC2254 # the pattern is meant to glob + case $_w in $_p) _out="${_out:+$_out }$_w"; break ;; esac + done + done + printf '%s' "$_out" +} +myos_filter_out() { + _pats=$(printf '%s' "$1" | tr '%' '*') + _out= + for _w in $2; do + _keep=yes + for _p in $_pats; do + # shellcheck disable=SC2254 # the pattern is meant to glob + case $_w in $_p) _keep=no; break ;; esac + done + [ "$_keep" = yes ] && _out="${_out:+$_out }$_w" + done + printf '%s' "$_out" +} + +# myos_addprefix PREFIX LIST / myos_addsuffix SUFFIX LIST +myos_addprefix() { _out=; for _w in $2; do _out="${_out:+$_out }$1$_w"; done; printf '%s' "$_out"; } +myos_addsuffix() { _out=; for _w in $2; do _out="${_out:+$_out }$_w$1"; done; printf '%s' "$_out"; } + +# myos_b64url read stdin, write url-safe base64 without padding +myos_b64url() { openssl enc -A -base64 | tr '+/' '-_' | tr -d '='; } + +# myos_jwt HEADER PAYLOAD SECRET a HS256 JSON web token +# Ported from the JWT macro of make/def.mk, which supabase uses to derive its +# anon and service keys from one secret. The make macro split on the commas of +# the payload; this one does not. +myos_jwt() { + _h=${1:-'{"alg":"HS256","typ":"JWT"}'} + _p=$2 + _s=$3 + _hb=$(printf '%s' "$_h" | myos_b64url) + _pb=$(printf '%s' "$_p" | myos_b64url) + _sig=$(printf '%s' "$_hb.$_pb" | openssl dgst -sha256 -binary -hmac "$_s" | myos_b64url) + printf '%s.%s.%s' "$_hb" "$_pb" "$_sig" +} diff --git a/spec/golden/expected/chain-build-up-logs.txt b/spec/golden/expected/chain-build-up-logs.txt index dea391f..70ba208 100644 --- a/spec/golden/expected/chain-build-up-logs.txt +++ b/spec/golden/expected/chain-build-up-logs.txt @@ -1,5 +1,5 @@ make -o docker-stack-build MAKE_OLDFILE=docker-stack-build ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-build STACK=myos APP_NAME=myos -docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local build --build-arg APP=myos --build-arg BRANCH=lightning --build-arg COMPOSE_VERSION=2.24.4 --build-arg DOCKER_MACHINE=x86_64 --build-arg DOCKER_REPOSITORY=tester/myos/local --build-arg DOCKER_SYSTEM=Linux --build-arg GIT_AUTHOR_EMAIL=tester@example.test --build-arg GIT_AUTHOR_NAME=tester --build-arg SSH_REMOTE_HOSTS=github.com gitlab.com --build-arg USER=tester --build-arg VERSION=legacy-1.0-beta-17-g55fae62 --build-arg SSH_AUTHORIZED_KEYS=https://github.com/tester.keys --build-arg SSH_BASTION_USERNAME=tester --build-arg SSH_PORT=22 --build-arg SSH_PUBLIC_HOSTS= github.com gitlab.com --build-arg SSH_USER=tester --build-arg GID=20 --build-arg UID=502 +docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local build --build-arg APP=myos --build-arg BRANCH=@BRANCH@ --build-arg COMPOSE_VERSION=2.24.4 --build-arg DOCKER_MACHINE=x86_64 --build-arg DOCKER_REPOSITORY=tester/myos/local --build-arg DOCKER_SYSTEM=Linux --build-arg GIT_AUTHOR_EMAIL=tester@example.test --build-arg GIT_AUTHOR_NAME=tester --build-arg SSH_REMOTE_HOSTS=github.com gitlab.com --build-arg USER=tester --build-arg VERSION=@VERSION@ --build-arg SSH_AUTHORIZED_KEYS=https://github.com/tester.keys --build-arg SSH_BASTION_USERNAME=tester --build-arg SSH_PORT=22 --build-arg SSH_PUBLIC_HOSTS= github.com gitlab.com --build-arg SSH_USER=tester --build-arg GID=20 --build-arg UID=502 make -o docker-stack-up MAKE_OLDFILE=docker-stack-up ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-up STACK=myos APP_NAME=myos sh -c docker network create tester-local >/dev/null 2>&1 sh -c docker network create testhost >/dev/null 2>&1 diff --git a/spec/support/run.sh b/spec/support/run.sh index 2689ad1..813e257 100644 --- a/spec/support/run.sh +++ b/spec/support/run.sh @@ -39,6 +39,8 @@ myos_normalize() { -e 's/^APPS .*/APPS @APPS@/' \ -e 's/^BRANCH .*/BRANCH @BRANCH@/' \ -e 's/^VERSION .*/VERSION @VERSION@/' \ + -e 's/--build-arg VERSION=[^ ]*/--build-arg VERSION=@VERSION@/' \ + -e 's/--build-arg BRANCH=[^ ]*/--build-arg BRANCH=@BRANCH@/' \ -e 's/[[:space:]][[:space:]]*/ /g' \ -e 's/[[:space:]]*$//' } diff --git a/spec/unit/str_spec.sh b/spec/unit/str_spec.sh index 3ab0b85..9cae319 100644 --- a/spec/unit/str_spec.sh +++ b/spec/unit/str_spec.sh @@ -68,3 +68,55 @@ Describe 'lib/str.sh' End End End + +# The make list functions the catalogue relies on, ported so that a stack hook +# reads like the .mk it replaces. +Describe 'lib/str.sh list functions' + It 'takes the first and last word' + When call myos_firstword "a b c" + The output should equal "a" + End + It 'takes the last word' + When call myos_lastword "a b c" + The output should equal "c" + End + It 'returns the first argument that is not empty' + When call myos_or "" "" "third" + The output should equal "third" + End + Describe 'myos_patsubst' + Parameters + "%" "pre%" "a b" "prea preb" + "%.yml" "%.yaml" "a.yml b.md" "a.yaml b.md" + "%" "%/" "x y" "x/ y/" + End + It "substitutes $1 -> $2 in $3" + When call myos_patsubst "$1" "$2" "$3" + The output should equal "$4" + End + End + It 'filters a list' + When call myos_filter "a%" "abc bcd axe" + The output should equal "abc axe" + End + It 'filters a list out' + When call myos_filter_out "local master main" "staging" + The output should equal "staging" + End + It 'drops the words it is told to' + When call myos_filter_out "local master main" "master" + The output should equal "" + End + It 'adds a prefix and a suffix' + When call myos_addprefix "x." "a b" + The output should equal "x.a x.b" + End +End + +Describe 'lib/str.sh myos_jwt' + It 'signs a token whose payload decodes back' + When call myos_jwt "" '{"role":"anon"}' secret + The output should start with "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + The output should include "eyJyb2xlIjoiYW5vbiJ9" + End +End