Compare commits

..
10 Commits
Author SHA1 Message Date
Yann Autissier 7163c844c9 note the catalogue conversion in the changelog 2026-09-03 22:33:29 +02:00
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
Yann Autissier 6192d73cbe 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.
2026-09-03 21:58:22 +02:00
Yann Autissier 3e55cdcd14 chain commands, and let a project refine a catalogue stack
myos build up logs host/fabio runs the three in order and stops at the first
failure, the way make build up logs STACK=host/fabio did. Leading words that
name commands are commands; the first word that is not one starts the stacks.

A stack found in several directories of the stack path is now merged rather
than shadowed, least specific first, so a project drops
stack/postgres/postgres.local.yml next to the catalogue's postgres.yml and
refines it. Settings hooks follow the same order, so a project can redefine a
default the catalogue ships. Neither engine did this before: the project
directory simply hid the catalogue one.

An unknown command now says so and suggests the command to type, instead of
printing the whole usage.
2026-09-03 21:17:17 +02:00
Yann Autissier 55fae625d6 keep the dynamism of make in pure shell
Two mechanisms, matching what the make engine actually did:

Lazy defaults. A stack setting is a function myos_default_<VAR>, called only
when the variable has no value, and called again at every reference. That is
exactly a recursive ?=: an explicit value wins, and the default follows a
DOMAIN that a .env changes later. The prefix is what makes it safe; the first
version used a bare function named after the variable, and the test suite
caught it running /usr/bin/host for a stack group called host.

Templates. myos env-update fills a .env from the .env.dist files, expanding
${VAR} against the current values and running $(command), forward references
included.

Also fixed: the project .env now wins over /etc/conf.d/myos, which is what the
documentation claimed and the code did not.

share/make/shim.mk lets a project keep make as a front end: every myos command
becomes a target that shells out to bin/myos, and the project keeps its own
targets and its stack .mk files. It sits outside make/ because the legacy
engine globs every .mk in there.
2026-09-03 20:46:28 +02:00
Yann Autissier f541ca418b let a stack compute its settings without make
A stack can now ship <name>.env and <name>.sh next to its compose files. The
hook is sourced with the tag helpers available, which is what the computing
.mk files of the catalogue were using make for: 29 of its 44 .mk files only
exist to build variables like the fabio tags.

Converting stack/host/fabio.mk by hand gives byte-identical output for the
route tag, and drops a trailing comma the make version left in the listener
list.

Also: APP_HOST and APP_URI are computed (the tag helpers build on them),
--color controls the escape codes rather than always emitting them, and
make test-portability runs the CLI under busybox ash and dash.
2026-09-03 20:22:52 +02:00
Yann Autissier 234739e531 fix the compose call: it never worked outside --dry-run
Setting IFS to a newline to pass the environment also stopped the command
line from splitting, so '-f a -f b' went out as a single argument and the two
words 'docker compose' were looked up as one program name. Every real command
was broken; only --dry-run, which prints a string, looked right.

The variables are now exported in a subshell instead of going through env(1),
which keeps values with spaces intact and leaves the command line to normal
word splitting.

The tests only checked what --dry-run printed, which is why they missed it.
spec/unit/compose_spec.sh now asserts the argv that is actually executed, for
both the plugin and the standalone binary.
2026-09-03 20:11:37 +02:00
Yann Autissier ac41e6e5f9 add the installer, and document the 2.0 CLI
install.sh handles a system or a user prefix, clones the catalogue on demand
and writes the machine config in the right place for the distribution.
2026-09-03 18:50:49 +02:00
Yann Autissier c08c379565 look for the catalogue beside the installation too
An install under a custom prefix (--prefix ~/.local) could not find the
catalogue it had just cloned into <prefix>/share/myos: only the three system
paths were searched.
2026-09-03 18:49:22 +02:00
Yann Autissier 32e2624245 add exec, run and scale, and the agent skill
The skill documents how to use myos: what to check before touching a host
stack, how a reference resolves, what the three kinds of stack mean, and where
each failure comes from. Every command it shows was run against the fixtures
before being written down, which is how exec, run and scale turned out to be
missing.

AGENTS.md covers the other side: how to change myos without breaking the
deployments that already run it.
2026-09-03 18:46:00 +02:00
78 changed files with 2250 additions and 251 deletions
+41
View File
@@ -0,0 +1,41 @@
# myos, for agents working on this repository
myos runs docker compose stacks. `skills/myos/SKILL.md` explains how to *use*
it; this file is about changing it.
## Check your work
```sh
make test # unit + golden, both engines, docker is mocked
make lint # shellcheck
```
Golden tests compare the output of the CLI **and** of the legacy make engine
against recordings made at the tag `legacy-1.0-beta`. If a change moves an
output on purpose, record the CLI expectation in `spec/golden/expected.cli/`
and write down why in `spec/golden/DELTAS.md`. Never edit
`spec/golden/expected/` by hand: it is the behaviour of the old engine.
## Layout
See `skills/myos/references/authoring.md`. In short: `bin/myos` parses and
dispatches, `lib/*.sh` holds one concern each, `lib/cmd/<name>.sh` holds one
command each, `share/compose/` holds the two overlays the framework itself
provides, and the stacks live in another repository, `myos-stacks`.
## Shell constraints
POSIX shell only: this runs on the bash 3.2 of macOS and on Alpine. No `local`,
no arrays, no `[[`. Two traps already paid for:
- `[a-z]` in a `case` pattern also matches uppercase under a `fr_FR` collation;
use `[:lower:]`.
- a function called inside `$( )` cannot return anything through a global.
## Do not
- Reintroduce a `stack/` or `docker/` directory here: they belong to
`myos-stacks`.
- Make an unknown command or an unknown stack succeed silently.
- Change a default project name without an entry in `DELTAS.md` and a note in
the skill: it renames the containers and volumes of every deployment.
+29
View File
@@ -1,5 +1,34 @@
# CHANGELOG # CHANGELOG
## v2.0.0-dev - 2026-09-03
- new bash CLI (`bin/myos`, `lib/`): one model for a project directory, a
catalogue stack, a group and a host singleton
- unknown command or unknown stack now fails, instead of succeeding silently
- default compose project is `<user>-<env>-<app>`; set
`MYOS_PROJECT_FORMAT=user-app-env` on deployments created before this
- `myos ls`, `myos env`, `myos doctor` to inspect an installation
- `install.sh`, and the catalogue is looked up beside the installation
- agent skill in `skills/myos/`, contributor notes in `AGENTS.md`
- stacks carry their settings in `<name>.env` and `<name>.sh` hooks, so the
catalogue no longer needs make to be installed
- lazy defaults (`myos_default_<VAR>` functions) give the recursive `?=` of
make in pure shell: an explicit value wins, and the default is recomputed
at each reference
- `myos env-update` generates a `.env` from the `.env.dist` templates,
expanding `${VAR}` and `$(command)`, including forward references
- 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
- 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
- a stack found in several directories of the stack path is merged, project
last, so a project refines a catalogue stack instead of replacing it
- `--color always|never|auto`, and no colour when the output is piped
- verified under the /bin/sh of Alpine (busybox) and Debian (dash)
- the make engine still works and is still covered by the golden tests
## v1.1 - 2026-09-03 ## v1.1 - 2026-09-03
- move the stack catalogue and the docker build contexts to the myos-stacks project - move the stack catalogue and the docker build contexts to the myos-stacks project
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+12 -1
View File
@@ -1,4 +1,4 @@
DEV_TARGETS := test test-unit test-golden test-integration lint golden-record DEV_TARGETS := test test-unit test-golden test-integration test-portability lint golden-record
ifneq ($(filter $(DEV_TARGETS),$(MAKECMDGOALS)),) ifneq ($(filter $(DEV_TARGETS),$(MAKECMDGOALS)),)
SHELLSPEC ?= shellspec SHELLSPEC ?= shellspec
@@ -16,6 +16,17 @@ test-integration: ## Run tests needing a real docker daemon
MYOS_INTEGRATION=1 $(SHELLSPEC) spec/integration MYOS_INTEGRATION=1 $(SHELLSPEC) spec/integration
golden-record: ## Re-record golden expectations from the legacy engine golden-record: ## Re-record golden expectations from the legacy engine
spec/golden/record.sh $(CASES) spec/golden/record.sh $(CASES)
test-portability: ## Run the CLI under the /bin/sh of Alpine and Debian
@for img in alpine:3.20 debian:13-slim; do \
printf '%s: ' "$$img"; \
tar cf - --exclude .git . | docker run -i --rm "$$img" /bin/sh -c \
'mkdir -p /myos && tar xf - -C /myos && cd /tmp && \
export PATH=/myos/spec/support/bin:$$PATH && \
/myos/bin/myos version >/dev/null && \
/myos/bin/myos -C /myos/spec/fixtures/host-project -n up host >/dev/null && \
echo ok'; \
done
lint: ## shellcheck all shell sources lint: ## shellcheck all shell sources
$(SHELLCHECK) -s bash myos spec/golden/record.sh spec/support/run.sh spec/support/bin/* $(wildcard bin/* lib/*.sh lib/cmd/*.sh install.sh) $(SHELLCHECK) -s bash myos spec/golden/record.sh spec/support/run.sh spec/support/bin/* $(wildcard bin/* lib/*.sh lib/cmd/*.sh install.sh)
+29 -4
View File
@@ -61,6 +61,9 @@ sudo git clone https://github.com/aya/myos-stacks /usr/local/share/myos
## Usage ## Usage
```sh ```sh
myos doctor # check the installation first
myos ls # what stacks are reachable
myos -n up host # print what it would run
myos up # the stack of the current directory myos up # the stack of the current directory
myos up STACK=host # a group of stacks, see stack/host/host.mk myos up STACK=host # a group of stacks, see stack/host/host.mk
myos up STACK=host/fabio # a single stack myos up STACK=host/fabio # a single stack
@@ -108,16 +111,38 @@ Networks: `default` = `_<project>` (private to the project), `private` =
| `ENV=<env>` | environment: selects `.env.<env>` and the `<name>.<env>.yml` overlays | | `ENV=<env>` | environment: selects `.env.<env>` and the `<name>.<env>.yml` overlays |
| `STACK=<refs>` | stacks to act on | | `STACK=<refs>` | stacks to act on |
| `SERVICE=<name>` | target one compose service (`exec`, `run`, `logs`, `scale`) | | `SERVICE=<name>` | target one compose service (`exec`, `run`, `logs`, `scale`) |
| `MYOS_PROJECT_FORMAT` | `user-env-app` (default) or `user-app-env` for deployments made before myos 2.0 |
```sh ```sh
myos print-COMPOSE_FILE # show a variable myos env COMPOSE_FILE # show a variable
myos print-COMPOSE_PROJECT_NAME myos env COMPOSE_PROJECT_NAME
myos debug # show debug variables myos config <stack> # the rendered compose file
myos doc # self documentation from the make comments
``` ```
The make targets keep working: `print-VAR`, `stack-<stack>-<command>`,
`<command>@<env>`, and a project `Makefile` that includes `make/include.mk`.
`SETUP_UFW=true` enables the ufw/ufw-docker integration (`myos setup-ufw`). `SETUP_UFW=true` enables the ufw/ufw-docker integration (`myos setup-ufw`).
## With make
A project that would rather drive make can include the shim, which turns every
myos command into a make target while leaving its own targets alone:
```make
MYOS ?= /usr/local/lib/myos
include $(MYOS)/share/make/shim.mk
```
`make up STACK=host` then runs exactly what `myos up host` runs: the shim only
forwards. make is not needed otherwise, and the CLI never calls it.
## For agents
`skills/myos/SKILL.md` is a skill describing how to drive myos, with
references on the conventions, the commands and the failure modes.
`AGENTS.md` covers changing myos itself.
## Tests ## Tests
```sh ```sh
+140 -45
View File
@@ -19,21 +19,33 @@ 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 tags naming stack config compose; do for _m in core str var tags naming stack config compose hooks context; do
# shellcheck source=/dev/null # shellcheck source=/dev/null
. "$MYOS_ROOT/lib/$_m.sh" . "$MYOS_ROOT/lib/$_m.sh"
done done
# myos_is_command WORD true when WORD names a command rather than a stack
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 ;;
print-*|stack-*-*) return 0 ;;
*@*) myos_is_command "${1%@*}"; return $? ;;
esac
[ -f "$MYOS_ROOT/lib/cmd/$1.sh" ]
}
# --- command line --------------------------------------------------------- # --- command line ---------------------------------------------------------
# These are read by the lib/cmd/* files sourced further down. # These are read by the lib/cmd/* files sourced further down.
# shellcheck disable=SC2034 # shellcheck disable=SC2034
{ {
MYOS_CMD= MYOS_CMDS=
MYOS_REFS= MYOS_REFS=
MYOS_REFS_RAW= MYOS_REFS_RAW=
MYOS_VARS= MYOS_VARS=
MYOS_ARGS= MYOS_ARGS=
MYOS_HOSTS= MYOS_HOSTS=
MYOS_COLOR=${MYOS_COLOR:-auto}
VERBOSE=${VERBOSE:-} VERBOSE=${VERBOSE:-}
DEBUG=${DEBUG:-} DEBUG=${DEBUG:-}
} }
@@ -47,6 +59,7 @@ Options:
-e ENV environment (default: local, or ENV from the config) -e ENV environment (default: local, or ENV from the config)
-H HOSTS run on remote hosts instead (comma separated, or "all") -H HOSTS run on remote hosts instead (comma separated, or "all")
-n, --dry-run print the commands instead of running them -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 -v, --verbose show what myos does
-d, --debug show every command -d, --debug show every command
-h, --help this help -h, --help this help
@@ -56,6 +69,7 @@ Commands:
ps logs config exec run inspect and enter them ps logs config exec run inspect and enter them
ls [--groups] list the stacks myos can see ls [--groups] list the stacks myos can see
env [VAR...] show resolved variables env [VAR...] show resolved variables
env-update fill .env from the .env.dist templates
doctor check the installation doctor check the installation
version print the myos version version print the myos version
@@ -73,55 +87,100 @@ while [ $# -gt 0 ]; do
-e) ENV=$2; shift 2 ;; -e) ENV=$2; shift 2 ;;
-H) MYOS_HOSTS=$2; shift 2 ;; -H) MYOS_HOSTS=$2; shift 2 ;;
-n|--dry-run) DRYRUN=true; shift ;; -n|--dry-run) DRYRUN=true; shift ;;
--color) MYOS_COLOR=$2; shift 2 ;;
--color=*) MYOS_COLOR=${1#--color=}; shift ;;
-v|--verbose) VERBOSE=true; shift ;; -v|--verbose) VERBOSE=true; shift ;;
-d|--debug) DEBUG=true; shift ;; -d|--debug) DEBUG=true; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
--) shift; MYOS_ARGS="$*"; break ;; --) shift; MYOS_ARGS="$*"; break ;;
-*) -*)
if [ -n "$MYOS_CMD" ]; then if [ -n "$MYOS_CMDS" ]; then
MYOS_ARGS="${MYOS_ARGS:+$MYOS_ARGS }$1"; shift MYOS_ARGS="${MYOS_ARGS:+$MYOS_ARGS }$1"; shift
else else
myos_error "unknown option: $1"; usage >&2; exit "$MYOS_E_USAGE" myos_error "unknown option: $1"; usage >&2; exit "$MYOS_E_USAGE"
fi ;; fi ;;
*=*) eval "${1%%=*}=\${1#*=}"; export "${1%%=*}"; shift ;; *=*) eval "${1%%=*}=\${1#*=}"; export "${1%%=*}"; shift ;;
*) *)
if [ -z "$MYOS_CMD" ]; then MYOS_CMD=$1; else MYOS_REFS="${MYOS_REFS:+$MYOS_REFS }$1"; fi # Leading words that name commands are commands, the rest are stacks:
# `myos build up logs host/fabio` runs three commands on one stack, the
# way `make build up logs STACK=host/fabio` did.
if [ -z "$MYOS_REFS" ] && myos_is_command "$1"; then
MYOS_CMDS="${MYOS_CMDS:+$MYOS_CMDS }$1"
else
MYOS_REFS="${MYOS_REFS:+$MYOS_REFS }$1"
fi
shift ;; shift ;;
esac esac
done done
[ -n "$MYOS_CMD" ] || { usage; exit "$MYOS_E_USAGE"; } if [ -z "$MYOS_CMDS" ]; then
if [ -n "$MYOS_REFS" ]; then
# the first word was meant as a command; name it rather than dump the usage
myos_error "unknown command: ${MYOS_REFS%% *}"
myos_error "to act on a stack of that name, say what to do: myos up ${MYOS_REFS%% *}"
exit "$MYOS_E_USAGE"
fi
usage
exit "$MYOS_E_USAGE"
fi
MYOS_REFS_RAW=$MYOS_REFS MYOS_REFS_RAW=$MYOS_REFS
case $MYOS_CMD in
env|ls|doctor) MYOS_VARS=$MYOS_REFS; MYOS_REFS= ;;
esac
# Targets of the make engine keep working: print-VAR, stack-<stack>-<command>, # Targets of the make engine keep working: print-VAR, stack-<stack>-<command>
# <command>@<env> and a bare group name. # and <command>@<env> each translate to a command of the CLI.
case $MYOS_CMD in _cmds=
for _c in $MYOS_CMDS; do
case $_c in
*@*) ENV=${_c#*@}; _c=${_c%@*} ;;
esac
case $_c in
print-*) print-*)
MYOS_VARS=${MYOS_CMD#print-} MYOS_VARS="${MYOS_VARS:+$MYOS_VARS }${_c#print-}"
# shellcheck disable=SC2209 # the literal string "env", not the command # shellcheck disable=SC2209 # the literal string "env", not the command
MYOS_CMD=env ;; _c=env ;;
*@*) ENV=${MYOS_CMD#*@}; MYOS_CMD=${MYOS_CMD%@*} ;;
esac
case $MYOS_CMD in
stack-*-*) stack-*-*)
_rest=${MYOS_CMD#stack-} _rest=${_c#stack-}
MYOS_CMD=${_rest##*-} _c=${_rest##*-}
MYOS_REFS="${_rest%-*} $MYOS_REFS" MYOS_REFS="${_rest%-*}${MYOS_REFS:+ $MYOS_REFS}" ;;
MYOS_REFS=${MYOS_REFS% } ;; esac
_cmds="${_cmds:+$_cmds }$_c"
done
MYOS_CMDS=$_cmds
# env, ls and doctor take variable names where the others take stacks
case $MYOS_CMDS in
env|ls|doctor)
[ -n "$MYOS_VARS" ] || MYOS_VARS=$MYOS_REFS
MYOS_REFS= ;;
esac esac
# --- configuration -------------------------------------------------------- # --- configuration --------------------------------------------------------
WORKDIR=${WORKDIR:-$PWD} WORKDIR=${WORKDIR:-$PWD}
WORKDIR=$(cd "$WORKDIR" 2>/dev/null && pwd -P) || myos_die "$MYOS_E_USAGE" "no such directory: $WORKDIR" WORKDIR=$(cd "$WORKDIR" 2>/dev/null && pwd -P) || myos_die "$MYOS_E_USAGE" "no such directory: $WORKDIR"
for _f in $(myos_conf_files); do myos_dotenv_load "$_f"; done # ENV decides which .env.<env> to read, so it is resolved first, from the most
myos_dotenv_load "${HOME:-}/.config/myos/config" # specific source that names it.
if [ -z "${ENV:-}" ]; then
for _f in "$WORKDIR/.env" "${HOME:-}/.config/myos/config" $(myos_conf_files); do
ENV=$(myos_dotenv_parse "$_f" | sed -n 's/^ENV=//p' | tail -1)
[ -n "$ENV" ] && break
done
fi
ENV=${ENV:-local} ENV=${ENV:-local}
myos_dotenv_load "$WORKDIR/.env.$ENV"
myos_dotenv_load "$WORKDIR/.env" # The layers, most specific first: the loader never overwrites a value, so the
# order below is the order of precedence. The environment and the VAR=value
# arguments are already set, and therefore win over every file.
# MYOS_CONF_PRIORITY=system puts the machine files first, as the make engine did.
myos_config_layers() {
if [ "${MYOS_CONF_PRIORITY:-}" = system ]; then
myos_conf_files
printf '%s\n' "${HOME:-}/.config/myos/config" "$WORKDIR/.env.$ENV" "$WORKDIR/.env"
else
printf '%s\n' "$WORKDIR/.env.$ENV" "$WORKDIR/.env" "${HOME:-}/.config/myos/config"
myos_conf_files
fi
}
for _f in $(myos_config_layers); do myos_dotenv_load "$_f"; done
# Overlay switches. Their names drive which <stack>.<suffix>.yml files load, # Overlay switches. Their names drive which <stack>.<suffix>.yml files load,
# so these defaults decide that e.g. supabase.labels.yml is picked up. # so these defaults decide that e.g. supabase.labels.yml is picked up.
@@ -154,26 +213,53 @@ if [ -z "$MYOS_REFS" ]; then
fi fi
fi fi
if [ -z "$MYOS_REFS" ]; then if [ -z "$MYOS_REFS" ]; then
case $MYOS_CMD in # these commands describe the installation rather than act on a stack
env|ls|doctor|version|help) ;; for _c in $MYOS_CMDS; do
case $_c in
env|env-update|ls|doctor|version|help) ;;
*) myos_die "$MYOS_E_USAGE" "no stack given, and no compose file in $WORKDIR" ;; *) myos_die "$MYOS_E_USAGE" "no stack given, and no compose file in $WORKDIR" ;;
esac esac
done
fi fi
# shellcheck disable=SC2086 # a list of references # shellcheck disable=SC2086 # a list of references
MYOS_STACKS=$(myos_group_expand $MYOS_REFS) 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)
MYOS_APP_FIRST=$(for _r in $MYOS_STACKS; do myos_stack_name "$_r"; break; done)
APP_HOST=${APP_HOST:-$(myos_app_host "$MYOS_SCOPE_FIRST" "$USER" "$ENV" "$MYOS_APP_FIRST" "$DOMAINNAME" "$HOSTNAME")}
APP_URI=${APP_URI:-$(myos_app_uri "$APP_HOST" "${APP_PATH:-}")}
APP_SCHEME=${APP_SCHEME:-http}
# Per-stack hooks must run in this shell: everything downstream reads the
# variables they set, and a command substitution would throw them away.
for _ref in $MYOS_STACKS; do
for _hdir in $(myos_stack_dirs "$_ref"); do
myos_stack_hooks "$_hdir" "$(myos_stack_name "$_ref")"
done
done
# The two functions below are called from the lib/cmd/* files sourced later,
# which shellcheck cannot see.
# shellcheck disable=SC2329
# myos_framework_compose_files the networks and volumes overlays myos itself # myos_framework_compose_files the networks and volumes overlays myos itself
# provides; they always come last so a stack can rely on them being there. # provides; they always come last so a stack can rely on them being there.
myos_framework_compose_files() { myos_framework_compose_files() {
myos_compose_files "$MYOS_ROOT/share/compose" "networks volumes" "$(myos_compose_suffixes)" "$ENV" myos_compose_files "$MYOS_ROOT/share/compose" "networks volumes" "$(myos_compose_suffixes)" "$ENV"
} }
# shellcheck disable=SC2329
# myos_stack_compose_files REF the ordered compose files of one reference # myos_stack_compose_files REF the ordered compose files of one reference
myos_stack_compose_files() { myos_stack_compose_files() {
_dir=$(myos_stack_resolve "$1") || return $? _dirs=$(myos_stack_dirs "$1")
[ -n "$_dirs" ] || { myos_stack_resolve "$1" >/dev/null; return $?; }
_name=$(myos_stack_name "$1") _name=$(myos_stack_name "$1")
_suffixes="$(myos_compose_suffixes) $(myos_stack_version "$1")" _suffixes="$(myos_compose_suffixes) $(myos_stack_version "$1")"
for _dir in $_dirs; do
case $1 in case $1 in
.|./*|/*|../*) .|./*|/*|../*)
myos_compose_files "$_dir" "docker-compose compose" "$_suffixes" "$ENV" myos_compose_files "$_dir" "docker-compose compose" "$_suffixes" "$ENV"
@@ -181,26 +267,35 @@ myos_stack_compose_files() {
*) *)
myos_compose_files "$_dir" "docker-compose $_name" "$_suffixes" "$ENV" ;; myos_compose_files "$_dir" "docker-compose $_name" "$_suffixes" "$ENV" ;;
esac esac
done
} }
# --- dispatch ------------------------------------------------------------- # --- dispatch -------------------------------------------------------------
case $MYOS_CMD in # myos_dispatch COMMAND run one command
version) printf 'myos %s\n' "$MYOS_VERSION"; exit 0 ;; myos_dispatch() {
help) usage; exit 0 ;; case $1 in
esac version) printf 'myos %s\n' "$MYOS_VERSION"; return 0 ;;
help) usage; return 0 ;;
# shellcheck source=/dev/null esac
if [ -f "$MYOS_ROOT/lib/cmd/$MYOS_CMD.sh" ]; then if [ -f "$MYOS_ROOT/lib/cmd/$1.sh" ]; then
. "$MYOS_ROOT/lib/cmd/$MYOS_CMD.sh" # shellcheck source=/dev/null
"myos_cmd_$MYOS_CMD" . "$MYOS_ROOT/lib/cmd/$1.sh"
exit $? "myos_cmd_$(printf '%s' "$1" | tr '-' '_')"
fi return $?
fi
# compose passthrough commands # commands that map straight onto docker compose
case $MYOS_CMD in case $1 in
up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) ;; up|down|start|stop|restart|ps|logs|config|build|pull|create|kill|top|images) ;;
*) myos_error "unknown command: $MYOS_CMD"; usage >&2; exit "$MYOS_E_USAGE" ;; *) myos_error "unknown command: $1"; usage >&2; return "$MYOS_E_USAGE" ;;
esac esac
# shellcheck source=/dev/null
. "$MYOS_ROOT/lib/cmd/_compose.sh"
myos_cmd_compose "$1"
}
. "$MYOS_ROOT/lib/cmd/_compose.sh" # Several commands run in order and stop at the first failure, as make did.
myos_cmd_compose "$MYOS_CMD" _rc=0
for MYOS_CMD in $MYOS_CMDS; do
myos_dispatch "$MYOS_CMD" || { _rc=$?; break; }
done
exit "$_rc"
Executable
+106
View File
@@ -0,0 +1,106 @@
#!/bin/sh
# myos installer.
#
# curl -fsSL https://raw.githubusercontent.com/aya/myos/lightning/install.sh | sh
# ... | sh -s -- --prefix ~/.local --with-stacks
#
# Installs the framework into <prefix>/lib/myos, links <prefix>/bin/myos, and
# optionally clones the stack catalogue into <prefix>/share/myos.
set -eu
MYOS_REPOSITORY=${MYOS_REPOSITORY:-https://github.com/aya/myos}
STACKS_REPOSITORY=${STACKS_REPOSITORY:-https://github.com/aya/myos-stacks}
REF=${MYOS_REF:-lightning}
PREFIX=
WITH_STACKS=false
WRITE_CONF=false
say() { printf '%s\n' "$*"; }
warn() { printf 'warning: %s\n' "$*" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
while [ $# -gt 0 ]; do
case $1 in
--prefix) PREFIX=$2; shift 2 ;;
--ref) REF=$2; shift 2 ;;
--repository) MYOS_REPOSITORY=$2; shift 2 ;;
--with-stacks) WITH_STACKS=true; shift ;;
--conf) WRITE_CONF=true; shift ;;
-h|--help)
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
# Default prefix: system wide when we can write there, user local otherwise.
if [ -z "$PREFIX" ]; then
if [ "$(id -u)" = 0 ] || [ -w /usr/local/lib ]; then PREFIX=/usr/local; else PREFIX=$HOME/.local; fi
fi
# --- requirements ---------------------------------------------------------
have git || die "git is required"
have docker || warn "docker not found: myos will not be able to run anything"
if have docker && docker compose version >/dev/null 2>&1; then :
elif have docker-compose; then :
else warn "no docker compose found: install the compose plugin, or docker-compose >= 2.24.4"
fi
# --- install --------------------------------------------------------------
LIB=$PREFIX/lib/myos
BIN=$PREFIX/bin
if [ -d "$LIB/.git" ]; then
say "updating $LIB"
git -C "$LIB" fetch --quiet origin "$REF"
git -C "$LIB" checkout --quiet FETCH_HEAD
else
say "installing myos into $LIB"
mkdir -p "$(dirname "$LIB")"
git clone --quiet --branch "$REF" "$MYOS_REPOSITORY" "$LIB"
fi
mkdir -p "$BIN"
ln -sf "$LIB/bin/myos" "$BIN/myos"
say "linked $BIN/myos"
if [ "$WITH_STACKS" = true ]; then
SHARE=$PREFIX/share/myos
if [ -d "$SHARE/.git" ]; then
say "updating the stack catalogue in $SHARE"
git -C "$SHARE" pull --quiet --ff-only
else
say "installing the stack catalogue into $SHARE"
mkdir -p "$(dirname "$SHARE")"
git clone --quiet "$STACKS_REPOSITORY" "$SHARE"
fi
fi
# --- machine configuration ------------------------------------------------
# /etc/conf.d on Alpine and other OpenRC systems, /etc/default elsewhere.
if [ "$WRITE_CONF" = true ]; then
if [ -d /etc/conf.d ]; then CONF=/etc/conf.d/myos; else CONF=/etc/default/myos; fi
if [ "$(id -u)" != 0 ]; then CONF=$HOME/.config/myos/config; mkdir -p "$(dirname "$CONF")"; fi
if [ -f "$CONF" ]; then
say "keeping the existing $CONF"
else
cat > "$CONF" <<CONFEOF
# myos machine settings, one KEY=value per line.
# ENV=master
# DOMAIN=$(hostname -d 2>/dev/null || echo example.org)
# WORKDIR=/srv/myos
# a deployment created before myos 2.0 must keep the old project names:
# MYOS_PROJECT_FORMAT=user-app-env
CONFEOF
say "wrote $CONF"
fi
fi
case :$PATH: in
*:$BIN:*) ;;
*) warn "$BIN is not on your PATH" ;;
esac
say ""
say "myos $("$BIN/myos" version 2>/dev/null | awk '{print $2}') installed"
say "next: myos doctor"
+26
View File
@@ -0,0 +1,26 @@
#shellcheck shell=sh
# myos env-update fill the .env of the workdir from the .env.dist it finds
#
# Templates are read from the workdir and from every requested stack, so a
# stack can ship the variables it expects and their defaults.
myos_cmd_env_update() {
_target=${ENV_FILE:-$WORKDIR/.env}
_dists=$MYOS_ARGS
if [ -z "$_dists" ]; then
for _ref in $MYOS_STACKS; do
_d=$(myos_stack_resolve "$_ref" 2>/dev/null) || continue
for _c in "$_d/.env.dist" "$_d/$(myos_stack_name "$_ref").env.dist"; do
[ -f "$_c" ] && _dists="${_dists:+$_dists }$_c"
done
done
for _c in "$WORKDIR/.env.dist" "$WORKDIR/.env.example" "$WORKDIR/.env.sample"; do
[ -f "$_c" ] && _dists="${_dists:+$_dists }$_c"
done
fi
[ -n "$_dists" ] || { myos_warning "no .env.dist found for $MYOS_STACKS"; return 0; }
for _dist in $_dists; do
myos_info "env-update $_target from $_dist"
myos_env_update "$_target" "$_dist" "$WORKDIR/.env.$ENV"
done
printf '%s\n' "$_target"
}
+32 -51
View File
@@ -1,67 +1,48 @@
#shellcheck shell=sh #shellcheck shell=sh
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos # shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
# myos env [VAR...] show resolved variables (replaces the make print-VAR target) # myos env [VAR...] show resolved variables (replaces the make print-VAR target)
# myos_env_print NAME VALUE
# Same shape as the make print-<VAR> target: the name padded to 37 columns in
# the highlight colour, then the value in the value colour.
myos_env_print() {
printf '%s%-37s%s%s%s%s\n' \
"$MYOS_C_HIGHLIGHT" "$1" "$MYOS_C_RESET" "$MYOS_C_VALUE" "$2" "$MYOS_C_RESET"
}
myos_cmd_env() { myos_cmd_env() {
_vars=${MYOS_VARS:-} _vars=${MYOS_VARS:-}
[ -n "$_vars" ] || _vars=$MYOS_ARGS [ -n "$_vars" ] || _vars=$MYOS_ARGS
[ -n "$_vars" ] || _vars=$MYOS_REFS_RAW [ -n "$_vars" ] || _vars=$MYOS_REFS_RAW
if [ -z "$_vars" ]; then if [ -z "$_vars" ]; then
_vars="ENV USER HOSTNAME DOMAIN WORKDIR MYOS_PATH STACK COMPOSE_PROJECT_NAME COMPOSE_FILE" _vars="ENV USER HOSTNAME DOMAIN WORKDIR MYOS_PATH SCOPE STACK COMPOSE_PROJECT_NAME COMPOSE_FILE"
fi fi
for _v in $_vars; do for _v in $_vars; do
case $_v in case $_v in
MYOS_PATH) printf '%s %s\n' "$_v" "$(myos_path)" ;; MYOS_PATH) myos_env_print "$_v" "$(myos_path)" ;;
COMPOSE_FILE) printf '%s %s\n' "$_v" "$(myos_all_compose_files | tr '\n' ' ' | sed 's/ $//')" ;; COMPOSE_FILE) myos_env_print "$_v" "$(myos_all_compose_files | tr '\n' ' ' | sed 's/ $//')" ;;
COMPOSE_PROJECT_NAME) printf '%s %s\n' "$_v" "$(myos_first_project)" ;; COMPOSE_PROJECT_NAME) myos_env_print "$_v" "$(myos_first_project)" ;;
STACK) printf '%s %s\n' "$_v" "$(printf '%s' "$MYOS_STACKS" | tr '\n' ' ' | sed 's/ $//')" ;; COMPOSE_SERVICE_NAME) myos_env_print "$_v" "$(myos_service_name "$(myos_first_project)")" ;;
DOCKER_NETWORK_DEFAULT) printf '%s %s\n' "$_v" "$(myos_network_default "$(myos_first_project)")" ;; COMPOSE_FILE_SUFFIX) myos_env_print "$_v" "$(myos_compose_suffixes)" ;;
DOCKER_NETWORK_PRIVATE) printf '%s %s\n' "$_v" "$(myos_network_private "$USER" "$ENV")" ;; STACK) myos_env_print "$_v" "$(printf '%s' "$MYOS_STACKS" | tr '\n' ' ' | sed 's/ $//')" ;;
DOCKER_NETWORK_PUBLIC) printf '%s %s\n' "$_v" "$(myos_network_public "${HOSTNAME:-}")" ;; SCOPE) myos_env_print "$_v" "$(myos_first_scope)" ;;
COMPOSE_SERVICE_NAME) printf '%s %s\n' "$_v" "$(myos_service_name "$(myos_first_project)")" ;; APP|APP_NAME) myos_env_print "$_v" "$(myos_first_app)" ;;
COMPOSE_FILE_SUFFIX) printf '%s %s\n' "$_v" "$(myos_compose_suffixes)" ;; DOCKER_REPOSITORY) myos_env_print "$_v" "$(printf '%s' "$(myos_first_project)" | tr '_-' '//')" ;;
APP|APP_NAME) printf '%s %s\n' "$_v" "$(myos_first_app)" ;; DOCKER_NETWORK_DEFAULT) myos_env_print "$_v" "$(myos_network_default "$(myos_first_project)")" ;;
SCOPE) printf '%s %s\n' "$_v" "$(myos_first_scope)" ;; DOCKER_NETWORK_PRIVATE) myos_env_print "$_v" "$(myos_network_private "$USER" "$ENV")" ;;
# HOST_STACK/USER_STACK are the names the make engine used for the scope DOCKER_NETWORK_PUBLIC) myos_env_print "$_v" "$(myos_network_public "${HOSTNAME:-}")" ;;
HOST_STACK) [ "$(myos_first_scope)" = host ] && printf '%s host\n' "$_v" || printf '%s\n' "$_v" ;;
USER_STACK) [ "$(myos_first_scope)" = user ] && printf '%s User\n' "$_v" || printf '%s\n' "$_v" ;;
DOCKER_REPOSITORY) printf '%s %s\n' "$_v" "$(printf '%s' "$(myos_first_project)" | tr '_-' '//')" ;;
DOCKER_NETWORK) DOCKER_NETWORK)
if [ "$(myos_first_scope)" = user ]; then printf '%s %s\n' "$_v" "$USER" if [ "$(myos_first_scope)" = user ]; then myos_env_print "$_v" "$USER"
else printf '%s %s\n' "$_v" "$(myos_network_private "$USER" "$ENV")"; fi ;; else myos_env_print "$_v" "$(myos_network_private "$USER" "$ENV")"; fi ;;
*) printf '%s %s\n' "$_v" "$(myos_var "$_v")" ;; # HOST_STACK and USER_STACK are what the make engine called the scope
HOST_STACK)
if [ "$(myos_first_scope)" = host ]; then myos_env_print "$_v" host
else myos_env_print "$_v" ""; fi ;;
USER_STACK)
if [ "$(myos_first_scope)" = user ]; then myos_env_print "$_v" User
else myos_env_print "$_v" ""; fi ;;
*) myos_env_print "$_v" "$(myos_var "$_v")" ;;
esac esac
done 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
}
+28
View File
@@ -0,0 +1,28 @@
#shellcheck shell=sh
# myos exec <stack> [-- command...] run a command in a running service
# myos run <stack> [-- command...] run it in a new container
#
# The service defaults to the stack name, which is what it is called in most
# stacks; SERVICE=<name> picks another one.
myos_cmd_exec() { myos_service_command exec; }
myos_cmd_run() { myos_service_command run; }
myos_service_command() {
_what=$1
_ref=$(printf '%s' "$MYOS_STACKS" | head -1)
[ -n "$_ref" ] || myos_die "$MYOS_E_USAGE" "usage: myos $_what <stack> [SERVICE=name] -- command..."
_service=${SERVICE:-$(myos_stack_name "$_ref")}
_files=$(myos_stack_compose_files "$_ref") || return $?
_fw=$(myos_framework_compose_files)
[ -n "$_fw" ] && _files="$_files
$_fw"
_app=$(myos_stack_name "$_ref")
case $_ref in .|./*|/*|../*) _app=$(basename "$(myos_stack_resolve "$_ref")") ;; esac
_project=$(myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$_app")
case $_what in
exec) _opts="" ;;
run) _opts=${DOCKER_COMPOSE_RUN_OPTIONS:---rm} ;;
esac
# shellcheck disable=SC2086 # options and arguments are deliberate word lists
myos_compose "$_project" "$_files" -- "$_what" $_opts "$_service" ${MYOS_ARGS:-}
}
+4
View File
@@ -0,0 +1,4 @@
#shellcheck shell=sh
# myos run: see lib/cmd/exec.sh
# shellcheck source=lib/cmd/exec.sh
. "$MYOS_ROOT/lib/cmd/exec.sh"
+13
View File
@@ -0,0 +1,13 @@
#shellcheck shell=sh
# shellcheck source=lib/cmd/_compose.sh
# myos scale <stack> SERVICE=<name> NUM=<n> run n containers of a service
. "$MYOS_ROOT/lib/cmd/_compose.sh"
myos_cmd_scale() {
_ref=$(printf '%s' "$MYOS_STACKS" | head -1)
[ -n "$_ref" ] || myos_die "$MYOS_E_USAGE" "usage: myos scale <stack> SERVICE=name NUM=n"
_service=${SERVICE:-$(myos_stack_name "$_ref")}
[ -n "${NUM:-}" ] || myos_die "$MYOS_E_USAGE" "myos scale needs NUM=<n>"
MYOS_ARGS="--scale $_service=$NUM ${MYOS_ARGS:-}"
myos_cmd_compose up
}
+19 -10
View File
@@ -39,22 +39,31 @@ myos_compose() {
done done
_dir=$(dirname "$_first") _dir=$(dirname "$_first")
# variables the compose files reference, plus the network names they default # the variables the compose files reference, plus the network names: those
# on (networks.yml is appended after the scan, so its variables are added here) # live in networks.yml, which is appended after the scan
# shellcheck disable=SC2086 # both are deliberate word lists # shellcheck disable=SC2086 # both are deliberate word lists
_vars=$(myos_env_vars $_files) _vars=$(myos_env_vars $_files)
# shellcheck disable=SC2086 # shellcheck disable=SC2086
_envargs=$(myos_env_export $_vars DOCKER_NETWORK_DEFAULT DOCKER_NETWORK_PRIVATE DOCKER_NETWORK_PUBLIC COMPOSE_SERVICE_NAME) _envargs=$(myos_env_export $_vars DOCKER_NETWORK_DEFAULT DOCKER_NETWORK_PRIVATE DOCKER_NETWORK_PUBLIC COMPOSE_SERVICE_NAME)
if [ "${DRYRUN:-false}" = true ]; then if [ "${DRYRUN:-false}" = true ]; then
# shellcheck disable=SC2086 # printed, not executed
printf '%s%s -p %s --project-directory %s %s\n' "$_bin" "$_fargs" "$_project" "$_dir" "$*" printf '%s%s -p %s --project-directory %s %s\n' "$_bin" "$_fargs" "$_project" "$_dir" "$*"
else return 0
_IFS=$IFS; IFS='
'
# shellcheck disable=SC2046,SC2086 # deliberate word splitting on IFS=newline
env $_envargs $_bin --ansi=auto $_fargs -p "$_project" --project-directory "$_dir" "$@"
_rc=$?
IFS=$_IFS
return $_rc
fi fi
# Export the variables in a subshell rather than through env(1): a value may
# contain spaces, and the command line must still be split on spaces (the
# compose binary can be the two words "docker compose").
(
while IFS= read -r _kv; do
[ -n "$_kv" ] || continue
# shellcheck disable=SC2163 # _kv is a NAME=value pair, not a name
export "$_kv"
done <<EOF
$_envargs
EOF
# shellcheck disable=SC2086 # _bin and _fargs are deliberate word lists
exec $_bin --ansi=auto $_fargs -p "$_project" --project-directory "$_dir" "$@"
)
} }
+91
View File
@@ -80,3 +80,94 @@ myos_env_export() {
done done
return 0 return 0
} }
# myos_expand STRING substitute ${VAR} and $(command) in STRING.
# shellcheck disable=SC2016 # the single quotes are deliberate: these patterns
# match the literal characters ${ and $( in the input, they are not expansions
# This is what the make engine did when it generated a .env out of a .env.dist:
# ${VAR} takes the current value, $(cmd) runs the command. Nothing else is
# interpreted, so the rest of the line can hold anything.
myos_expand() {
_in=$1
_guard=0
while [ "$_guard" -lt 16 ]; do
_guard=$((_guard + 1))
case $_in in
*'${'*'}'*)
_pre=${_in%%'${'*}
_rest=${_in#*'${'}
_name=${_rest%%\}*}
_post=${_rest#*\}}
case $_name in
''|*[!A-Za-z0-9_]*) _in="$_pre\${$_name}$_post"; break ;;
esac
_in="$_pre$(myos_var "$_name")$_post" ;;
*) break ;;
esac
done
_guard=0
while [ "$_guard" -lt 16 ]; do
_guard=$((_guard + 1))
case $_in in
*'$('*')'*)
_pre=${_in%%'$('*}
_rest=${_in#*'$('}
_cmd=${_rest%%)*}
_post=${_rest#*)}
_in="$_pre$(eval "$_cmd" 2>/dev/null)$_post" ;;
*) break ;;
esac
done
printf '%s' "$_in"
}
# myos_env_update FILE DIST [OVER...]
# Add to FILE every variable of DIST that is missing from it, expanded.
# A variable that already has a value keeps it, whether it comes from the
# environment, from FILE, or from one of the OVER files: the .env is a record
# of the choices already made, never something that overwrites them.
myos_env_update() {
_file=$1; _dist=$2; shift 2
[ -f "$_dist" ] || return 0
[ -e "$_file" ] || : > "$_file"
# what the overrides pin, read before anything else
for _over in "$@"; do
[ -f "$_over" ] || continue
myos_dotenv_load "$_over"
done
# Which keys already hold a choice, made in the environment, in the .env or
# in an override. Those are kept verbatim; everything else is a template.
_preset=" "
while IFS= read -r _kv; do
[ -n "$_kv" ] || continue
_k=${_kv%%=*}
[ -n "$_k" ] || continue
[ -n "$(myos_var "$_k")" ] && _preset="$_preset$_k "
done <<EOF
$(myos_dotenv_parse "$_dist")
EOF
# Make the templates themselves visible, so a line may refer to a variable
# defined further down the file, as the make engine allowed.
myos_dotenv_load "$_dist"
_added=0
while IFS= read -r _kv; do
[ -n "$_kv" ] || continue
_k=${_kv%%=*}
[ -n "$_k" ] || continue
myos_dotenv_has "$_file" "$_k" && continue
case $_preset in
*" $_k "*) _v=$(myos_var "$_k") ;;
*) _v=$(myos_expand "${_kv#*=}") ;;
esac
printf '%s=%s\n' "$_k" "$_v" >> "$_file"
_added=$((_added + 1))
done <<EOF
$(myos_dotenv_parse "$_dist")
EOF
[ "$_added" -gt 0 ] && myos_info "added $_added variable(s) to $_file"
return 0
}
+68
View File
@@ -0,0 +1,68 @@
#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() { myos_addprefix "${HOSTNAME:-}." "$(myos_var DOMAIN)"; }
myos_default_HOSTNAME() { printf '%s' "${HOSTNAME:-}"; }
myos_default_DOMAINNAME() { myos_firstword "$(myos_var DOMAIN)"; }
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:-}"; }
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:-}"; }
}
+13 -1
View File
@@ -13,13 +13,25 @@ MYOS_E_USAGE=2 # bad invocation
MYOS_E_NOSTACK=3 # stack not found MYOS_E_NOSTACK=3 # stack not found
MYOS_E_NOREQ=4 # missing requirement MYOS_E_NOREQ=4 # missing requirement
# myos_colors decide whether to emit colour.
# MYOS_COLOR=always|never|auto (default auto: only when stdout is a terminal).
# The make engine always emitted the escape codes, even into a pipe.
myos_colors() { myos_colors() {
if [ -t 2 ] && [ "${TERM:-dumb}" != dumb ] && [ -z "${NO_COLOR:-}" ]; then _want=${MYOS_COLOR:-auto}
[ -n "${NO_COLOR:-}" ] && _want=never
case $_want in
never) _want=no ;;
always) _want=yes ;;
*) if [ -t 1 ] && [ "${TERM:-dumb}" != dumb ]; then _want=yes; else _want=no; fi ;;
esac
if [ "$_want" = yes ]; then
MYOS_C_ERROR=$(printf '\033[31m'); MYOS_C_WARN=$(printf '\033[01;33m') MYOS_C_ERROR=$(printf '\033[31m'); MYOS_C_WARN=$(printf '\033[01;33m')
MYOS_C_INFO=$(printf '\033[33m'); MYOS_C_DEBUG=$(printf '\033[01;34m') MYOS_C_INFO=$(printf '\033[33m'); MYOS_C_DEBUG=$(printf '\033[01;34m')
MYOS_C_VALUE=$(printf '\033[36m'); MYOS_C_RESET=$(printf '\033[0m') MYOS_C_VALUE=$(printf '\033[36m'); MYOS_C_RESET=$(printf '\033[0m')
MYOS_C_HIGHLIGHT=$(printf '\033[32m')
else else
MYOS_C_ERROR=; MYOS_C_WARN=; MYOS_C_INFO=; MYOS_C_DEBUG=; MYOS_C_VALUE=; MYOS_C_RESET= MYOS_C_ERROR=; MYOS_C_WARN=; MYOS_C_INFO=; MYOS_C_DEBUG=; MYOS_C_VALUE=; MYOS_C_RESET=
MYOS_C_HIGHLIGHT=
fi fi
} }
+69
View File
@@ -0,0 +1,69 @@
#shellcheck shell=sh
# shellcheck disable=SC1090 # hooks are sourced by a path built at run time
# hooks: the per-stack settings that used to live in a .mk file.
#
# A stack directory may ship, next to its compose files:
# _stack.env settings shared by every stack of the directory
# _stack.sh the same, computed
# <name>.env settings of one stack
# <name>.env.<env> the same, for one environment
# <name>.sh computed settings of one stack
# <name>.mk the legacy make snippet, still read for its groups
#
# A .sh hook declares lazy defaults (see lib/var.sh): functions named
# myos_default_<VARIABLE>, called only when the variable has no value and
# called again at each reference. That is a make `?=` on a recursive variable,
# and it is what lets a stack of the catalogue work without make installed.
#
# The _stack hooks of every directory between the stack path root and the stack
# itself are loaded, outermost first: make included both $(dir)/*.mk and
# $(dir)/*/*.mk, so a stack in a subdirectory saw its parent's settings.
# myos_stack_hooks DIR NAME load the hooks that apply to one stack
myos_stack_hooks() {
_hdir=$1; _hname=$2
# the stack path entry this directory belongs to
_root=
_IFS=$IFS; IFS=:
for _r in $(myos_path); do
IFS=$_IFS
case $_hdir in "$_r"|"$_r"/*) _root=$_r; break ;; esac
IFS=:
done
IFS=$_IFS
# every directory from the root down to the stack, outermost first
_chain=$_hdir
if [ -n "$_root" ]; then
_d=$_hdir
while [ "$_d" != "$_root" ] && [ "$_d" != "/" ] && [ -n "$_d" ]; do
_d=$(dirname "$_d")
_chain="$_d
$_chain"
done
fi
for _d in $_chain; do
myos_hook_load "$_d/_stack.env" dotenv
myos_hook_load "$_d/_stack.sh" shell
done
myos_hook_load "$_hdir/$_hname.env" dotenv
myos_hook_load "$_hdir/$_hname.env.$ENV" dotenv
myos_hook_load "$_hdir/$_hname.sh" shell
myos_hook_load "$_hdir/$_hname.$ENV.sh" shell
return 0
}
# myos_hook_load FILE KIND
myos_hook_load() {
[ -f "$1" ] || return 0
myos_debug "hook $1"
# a hook may need to read a file it ships next to itself
# shellcheck disable=SC2034 # read by the hooks sourced below
MYOS_STACK_DIR=$(dirname "$1")
case $2 in
dotenv) myos_dotenv_load "$1" ;;
shell) . "$1" ;;
esac
}
+41
View File
@@ -79,3 +79,44 @@ myos_service_name() { printf '%s' "$1" | tr '_' '-'; }
myos_network_default() { printf '_%s' "$1"; } myos_network_default() { printf '_%s' "$1"; }
myos_network_private() { printf '%s' "${DOCKER_NETWORK_PRIVATE:-${1}-${2}}"; } myos_network_private() { printf '%s' "${DOCKER_NETWORK_PRIVATE:-${1}-${2}}"; }
myos_network_public() { printf '%s' "${DOCKER_NETWORK_PUBLIC:-${1}}"; } myos_network_public() { printf '%s' "${DOCKER_NETWORK_PUBLIC:-${1}}"; }
# myos_app_domain SCOPE USER DOMAIN
# The domain a stack is served on. A host stack is never prefixed by the user:
# it belongs to the machine.
myos_app_domain() {
_scope=$1; _u=$2; _dom=$3
if [ "$_scope" != host ] && [ "${APP_HOST_MULTI_USER:-false}" = true ]; then
printf '%s.%s' "$_u" "$_dom"
else
printf '%s' "$_dom"
fi
}
# myos_app_host SCOPE USER ENV APP DOMAIN HOSTNAME
# A host stack is served on <hostname>.<domain>; anything else is prefixed by
# the environment unless the environment is the main one.
myos_app_host() {
_scope=$1; _u=$2; _env=$3; _app=$4; _dom=$5; _host=$6
_multi_env=${APP_HOST_MULTI_ENV:-}
if [ -z "$_multi_env" ]; then
case $_env in local|master|main) _multi_env=false ;; *) _multi_env=true ;; esac
fi
_prefix=
if [ "$_scope" = host ]; then _prefix="$_host."
elif [ "$_multi_env" = true ]; then _prefix="$_env."
fi
_name=
[ "${APP_HOST_MULTI_APP:-false}" = true ] && _name="$(myos_name "$_app")."
_out="$_prefix$_name$(myos_app_domain "$_scope" "$_u" "$_dom")"
# a host stack behind the load balancer also answers on the bare domain
[ "$_scope" = host ] && [ -n "${HOST_LB:-}" ] && _out="$_out $_dom"
printf '%s' "$_out"
}
# myos_app_uri HOST [PATH] the base uri the fabio tags are built on;
# it always ends with a slash
myos_app_uri() {
_out=
for _h in $1; do _out="${_out:+$_out }$_h/${2:-}"; done
printf '%s' "$_out"
}
+43 -4
View File
@@ -15,7 +15,12 @@ myos_path() {
_wd=${WORKDIR:-$PWD} _wd=${WORKDIR:-$PWD}
_name=${STACK_DIR_NAME:-stack} _name=${STACK_DIR_NAME:-stack}
_out= _out=
for _d in "$_wd" "$_wd/.." "${HOME:-/nonexistent}/.local/share" /usr/local/share /usr/share; do # <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 for _c in "$_d/$_name" "$_d/myos/$_name"; do
[ -d "$_c" ] || continue [ -d "$_c" ] || continue
_c=$(cd "$_c" && pwd -P) _c=$(cd "$_c" && pwd -P)
@@ -39,6 +44,37 @@ myos_stack_version() {
case $_r in *:*) printf '%s' "${_r##*:}" ;; *) printf 'latest' ;; esac 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, # myos_stack_resolve REF print the directory holding the stack,
# or fail with MYOS_E_NOSTACK # or fail with MYOS_E_NOSTACK
myos_stack_resolve() { myos_stack_resolve() {
@@ -135,9 +171,12 @@ myos_group_value() {
_IFS=$IFS; IFS=: _IFS=$IFS; IFS=:
for _d in $(myos_path); do for _d in $(myos_path); do
IFS=$_IFS IFS=$_IFS
[ -f "$_d/$1.env" ] && { _v=$(sed -n "s/^$1=//p" "$_d/$1.env" | tail -1 | tr -d '"'); } for _f in "$_d/$1.env" "$_d/$1/$1.env" "$_d/$1/_stack.env"; do
[ -z "$_v" ] && [ -f "$_d/$1.mk" ] && { _v=$(myos_mk_group "$_d/$1.mk" "$1"); } [ -z "$_v" ] && [ -f "$_f" ] && _v=$(sed -n "s/^$1=//p" "$_f" | tail -1 | tr -d '"')
[ -z "$_v" ] && [ -f "$_d/$1/$1.mk" ] && { _v=$(myos_mk_group "$_d/$1/$1.mk" "$1"); } 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; } [ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
IFS=: IFS=:
done done
+102
View File
@@ -14,8 +14,10 @@ myos_slugify() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9
# myos_reverse WORDS... reverse the order of space separated words # myos_reverse WORDS... reverse the order of space separated words
myos_reverse() { myos_reverse() {
set -f # a word may be a pattern such as *.example.org, do not glob it
_out= _out=
for _w in $1; do _out="$_w${_out:+ }$_out"; done for _w in $1; do _out="$_w${_out:+ }$_out"; done
set +f
printf '%s' "$_out" printf '%s' "$_out"
} }
@@ -31,3 +33,103 @@ myos_verlt() {
[ "$1" = "$2" ] && return 1 [ "$1" = "$2" ] && return 1
myos_verle "$1" "$2" myos_verle "$1" "$2"
} }
# The make list functions the catalogue uses, on space separated words.
# myos_firstword LIST / myos_lastword LIST
myos_firstword() { set -f; for _w in $1; do set +f; printf '%s' "$_w"; return 0; done; set +f; }
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() {
set -f # a word may be a pattern such as *.example.org, do not glob it
_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
set +f
printf '%s' "$_out"
}
# myos_pattern MAKE_PATTERN the shell pattern matching a make pattern.
# In make only % is a wildcard, so a literal *, ? or [ has to be protected
# before % becomes *: "*.%" means "starts with a star and a dot", not
# "anything".
myos_pattern() {
printf '%s' "$1" | sed -e 's/[][*?]/\\&/g' -e 's/%/*/g'
}
# myos_filter PATTERNS LIST / myos_filter_out PATTERNS LIST
myos_filter() {
set -f # a word may itself be a pattern, do not glob it
_out=
for _w in $2; do
for _raw in $1; do
_p=$(myos_pattern "$_raw")
# shellcheck disable=SC2254 # the pattern is meant to match, not to glob
case $_w in $_p) _out="${_out:+$_out }$_w"; break ;; esac
done
done
set +f
printf '%s' "$_out"
}
myos_filter_out() {
set -f
_out=
for _w in $2; do
_keep=yes
for _raw in $1; do
_p=$(myos_pattern "$_raw")
# shellcheck disable=SC2254 # the pattern is meant to match, not to glob
case $_w in $_p) _keep=no; break ;; esac
done
[ "$_keep" = yes ] && _out="${_out:+$_out }$_w"
done
set +f
printf '%s' "$_out"
}
# myos_addprefix PREFIX LIST / myos_addsuffix SUFFIX LIST
myos_addprefix() { set -f; _out=; for _w in $2; do _out="${_out:+$_out }$1$_w"; done; set +f; printf '%s' "$_out"; }
myos_addsuffix() { set -f; _out=; for _w in $2; do _out="${_out:+$_out }$_w$1"; done; set +f; 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"
}
# myos_patsublist PATTERN REPLACEMENT LIST
# patsubst over a list, joined by commas. The fabio tags are built this way:
# one route per uri, in a single label.
myos_patsublist() {
set -f # a word may be a pattern such as *.example.org, do not glob it
_out=
for _w in $3; do
_out="${_out:+$_out,}$(myos_patsubst "$1" "$2" "$_w")"
done
set +f
printf '%s' "$_out"
}
+33 -11
View File
@@ -4,42 +4,49 @@
# Ported from make/apps/def.mk (uri, url, urlprefix, urlprefixs, tagprefix, # Ported from make/apps/def.mk (uri, url, urlprefix, urlprefixs, tagprefix,
# envprefix, servicenvs). Registrator publishes the SERVICE_<port>_TAGS label # envprefix, servicenvs). Registrator publishes the SERVICE_<port>_TAGS label
# to consul, fabio routes on the urlprefix- tags it finds there. # to consul, fabio routes on the urlprefix- tags it finds there.
#
# Every lookup goes through myos_var (lib/var.sh), so a stack setting may be a
# plain value or a lazy default, and the two behave the same here.
# myos_var NAME value of the variable named NAME, empty when unset # myos_uri SERVICES PORT [BASE_URI]
myos_var() { # <service>.<base uri> for each service and each base uri, unless
[ -n "${1:-}" ] || return 0 # <SERVICE>_SERVICE[_<port>]_NAME overrides the prefix. The first argument is a
eval "printf '%s' \"\${$1:-}\"" # list: one stack may publish several services on one port.
}
# myos_uri SERVICE PORT [BASE_URI]
# <service>.<base uri>, unless <SERVICE>_SERVICE[_<port>]_NAME overrides the prefix
myos_uri() { myos_uri() {
_svc=$1; _port=${2:-}; _base=${3:-${APP_URI:-}} set -f # a uri may be a pattern such as *.ipns.example.org
_svcs=$1; _port=${2:-}; _base=${3:-${APP_URI:-}}
_out=
for _svc in $_svcs; do
_u=$(myos_upper "$_svc") _u=$(myos_upper "$_svc")
_name=$(myos_var "${_u}_SERVICE_${_port}_NAME") _name=$(myos_var "${_u}_SERVICE_${_port}_NAME")
[ -n "$_name" ] || _name=$(myos_var "${_u}_SERVICE_NAME") [ -n "$_name" ] || _name=$(myos_var "${_u}_SERVICE_NAME")
[ -n "$_name" ] || _name=$_svc [ -n "$_name" ] || _name=$_svc
_out=
for _b in $_base; do _out="${_out:+$_out }${_name}.${_b}"; done for _b in $_base; do _out="${_out:+$_out }${_name}.${_b}"; done
done
set +f
printf '%s' "$_out" printf '%s' "$_out"
} }
# myos_url SERVICE PORT [BASE_URI] # myos_url SERVICE PORT [BASE_URI]
myos_url() { myos_url() {
set -f
_out= _out=
for _u in $(myos_uri "$@"); do _out="${_out:+$_out }${APP_SCHEME:-http}://$_u"; done for _u in $(myos_uri "$@"); do _out="${_out:+$_out }${APP_SCHEME:-http}://$_u"; done
set +f
printf '%s' "$_out" printf '%s' "$_out"
} }
# myos_urlprefix [PATH] [OPTS] [URIS] # myos_urlprefix [PATH] [OPTS] [URIS]
# one comma separated "urlprefix-<uri><path>* [opts]" per uri # one comma separated "urlprefix-<uri><path>* [opts]" per uri
myos_urlprefix() { myos_urlprefix() {
set -f
_path=${1:-}; _opts=${2:-}; _uris=${3:-${APP_URI:-}} _path=${1:-}; _opts=${2:-}; _uris=${3:-${APP_URI:-}}
_out= _out=
for _u in $_uris; do for _u in $_uris; do
_tag="urlprefix-${_u}${_path}${MYOS_URL_SUFFIX:-*}${_opts:+ $_opts}" _tag="urlprefix-${_u}${_path}${MYOS_URL_SUFFIX:-*}${_opts:+ $_opts}"
_out="${_out:+$_out,}$_tag" _out="${_out:+$_out,}$_tag"
done done
set +f
printf '%s' "$_out" printf '%s' "$_out"
} }
@@ -65,9 +72,24 @@ myos_tagprefix() {
[ -n "$_opts" ] || _opts=$(myos_envprefix "$_stack" "$_port" allow auth deny prepend proto register strip) [ -n "$_opts" ] || _opts=$(myos_envprefix "$_stack" "$_port" allow auth deny prepend proto register strip)
_uris= _uris=
for _k in "$@"; do for _k in "$@"; do
_v=$(myos_var "${_u}_SERVICE_${_port}_${_k}"); [ -n "$_v" ] && _uris="${_uris:+$_uris }$_v" _v=$(myos_var "${_u}_SERVICE_${_port}_$(myos_upper "$_k")")
[ -n "$_v" ] && _uris="${_uris:+$_uris }$_v"
done done
[ -n "$_uris" ] || _uris=$(myos_var "${_u}_SERVICE_${_port}_URIS") [ -n "$_uris" ] || _uris=$(myos_var "${_u}_SERVICE_${_port}_URIS")
[ -n "$_uris" ] || _uris=$(myos_uri "$_stack" "$_port") [ -n "$_uris" ] || _uris=$(myos_uri "$_stack" "$_port")
myos_urlprefix "$_path" "$_opts" "$_uris" myos_urlprefix "$_path" "$_opts" "$_uris"
} }
# myos_servicenvs STACK GROUP KEY
# Collect <STACK>_SERVICE_<env>_<KEY> for every env listed in
# <STACK>_SERVICE_<GROUP>_ENVS. Used to build a list of listeners out of one
# variable per protocol.
myos_servicenvs() {
_s=$(myos_upper "$1"); _g=$(myos_upper "$2"); _k=$(myos_upper "$3")
_out=
for _e in $(myos_var "${_s}_SERVICE_${_g}_ENVS"); do
_v=$(myos_var "${_s}_SERVICE_$(myos_upper "$_e")_${_k}")
[ -n "$_v" ] && _out="${_out:+$_out }$_v"
done
printf '%s' "$_out"
}
+56
View File
@@ -0,0 +1,56 @@
#shellcheck shell=sh
# var: variable resolution, with the lazy defaults the make engine had.
#
# make gives every `VAR ?= $(call ...)` two properties at once: an explicit
# value wins, and the default is re-evaluated at each reference, so it sees
# whatever a .env loaded later has changed.
#
# The shell gets both by keeping defaults in functions: `myos_var NAME` reads
# the variable when it has a value, and otherwise calls the function
# `myos_default_NAME`. The prefix matters: a bare function named after the
# variable would collide with commands on PATH, and a stack setting called
# `host` or `test` would then run a program instead of returning a value.
#
# myos_default_HOST_FABIO_SERVICE_9998_TAGS() { myos_tagprefix HOST_FABIO 9998; }
#
# is the exact equivalent of
#
# HOST_FABIO_SERVICE_9998_TAGS ?= $(call tagprefix,HOST_FABIO,9998)
MYOS_VAR_MAX_DEPTH=${MYOS_VAR_MAX_DEPTH:-32}
# myos_var NAME the value of NAME: the variable if it has one, else the lazy
# default, else empty.
myos_var() {
[ -n "${1:-}" ] || return 0
eval "_myos_set=\${$1+yes}"
if [ "${_myos_set:-}" = yes ]; then
eval "printf '%s' \"\$$1\""
return 0
fi
myos_var_is_lazy "$1" || return 0
# a default written in terms of itself would loop for ever
_myos_depth=$((${MYOS_VAR_DEPTH:-0} + 1))
if [ "$_myos_depth" -gt "$MYOS_VAR_MAX_DEPTH" ]; then
myos_error "variable $1 is defined in terms of itself"
return 1
fi
MYOS_VAR_DEPTH=$_myos_depth "myos_default_$1"
}
# myos_default NAME BODY declare a lazy default from a string, for callers
# that build the variable name at run time
myos_default() {
eval "myos_default_$1() { $2; }"
}
# myos_var_is_lazy NAME true when NAME has no value but has a lazy default
myos_var_is_lazy() {
eval "_myos_set=\${$1+yes}"
[ "${_myos_set:-}" = yes ] && return 1
# a shell function, never a command on PATH: command -v prints the name back
# for a function and a path for a program
_myos_fn=$(command -v "myos_default_$1" 2>/dev/null) || return 1
[ "$_myos_fn" = "myos_default_$1" ]
}
+65
View File
@@ -0,0 +1,65 @@
##
# myos, from make.
#
# This file gives a project the myos commands as make targets, without the make
# engine: every target shells out to bin/myos, which is pure POSIX sh. What the
# project keeps from make is what make is actually good at, and the CLI is not:
# its own targets, its own dependencies, and the .mk files of its stacks.
#
# MYOS ?= /usr/local/lib/myos
# include $(MYOS)/share/make/shim.mk
#
# It lives outside make/ on purpose: the legacy engine includes every .mk of
# that directory, and would pull this one in too.
#
# Then `make up STACK=host` and `myos up host` do the same thing, through the
# same code. Variables given on the command line are forwarded, so
# `make up STACK=host DOMAIN=example.org` behaves as expected.
MYOS ?= $(patsubst %/share/make/shim.mk,%,$(lastword $(MAKEFILE_LIST)))
MYOS_BIN ?= $(MYOS)/bin/myos
STACK_DIR_NAME ?= stack
## every directory myos looks for stacks in, so the .mk of a stack installed
## system wide brings its targets along too
STACK_DIR ?= $(subst :, ,$(shell $(MYOS_BIN) env MYOS_PATH --color=never 2>/dev/null | awk '{print $$2}'))
# variable MYOS_ARGS: variables set on the make command line, forwarded to myos
MYOS_ARGS ?= $(foreach v,$(MAKEOVERRIDES),$(v))
.DEFAULT_GOAL := help
## the stack files may add their own targets: that is what make is kept for
include $(foreach dir,$(STACK_DIR),$(wildcard $(dir)/*.mk $(dir)/*/*.mk))
# function make: run a myos command, for the stack .mk files that call it
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)
myos-var = $(shell $(MYOS_BIN) --color=never $(MYOS_ARGS) env $(1) | awk '{print $$2}')
# target help: List the myos commands
.PHONY: help
help:
@$(MYOS_BIN) help
# target myos: Run an arbitrary myos command, as in `make myos ARGS="up host"`
.PHONY: myos
myos:
@$(MYOS_BIN) $(MYOS_ARGS) $(ARGS)
# make tries to remake every makefile it read, and the catch-all below would
# hand each of them to myos as a command. An empty rule stops that.
$(MAKEFILE_LIST): ;
# target %: Hand anything else to myos
## a target the project defines itself keeps precedence over this rule
%: FORCE
@$(MYOS_BIN) $(MYOS_ARGS) $@ $(ARGS)
.PHONY: FORCE
FORCE: ;
+98
View File
@@ -0,0 +1,98 @@
---
name: myos
description: Deploy, inspect and troubleshoot docker compose stacks on a server or a fleet with the myos CLI (Make Your Own Stack). Use when asked to install myos, to start/stop/debug a stack (a host singleton, an application, a user stack), to add a stack to the catalogue, or to work on the myos framework itself.
---
# myos
myos runs docker compose stacks. It decides which compose files to load, under
which project name, with which variables. Everything else is docker.
## Is this the right tool
Use it when the repository or the host has one of:
- a `stack/` directory holding compose files,
- a `Makefile` that includes `make/include.mk`,
- `/etc/conf.d/myos` or `/etc/default/myos`,
- the `myos` command on PATH.
Otherwise call `docker compose` directly.
## Look before you touch
```sh
myos doctor # requirements, config layers, stack path
myos ls # stacks reachable from here
myos ls --groups # groups and what they expand to
myos env # resolved variables
myos -n up host # print what `up` would run, without running it
```
**Always run `myos -n` before `up`, `down` or `restart` on a `host/` stack.**
Host stacks bind privileged ports and are shared by everything on the machine:
taking `host/fabio` down drops every site the machine serves.
## Working loop
```sh
myos ls # find the stack
myos -n config <stack> # check the file list and the project name
myos up <stack> # create and start
myos ps <stack> # what is running
myos logs <stack> # follow the logs
myos exec <stack> -- sh # a shell in the service named after the stack
myos down <stack> # remove the containers
```
## Naming a stack
A reference is `[<group>/]<name>[:<version>]`, or a path.
Commands chain: `myos build up logs host/fabio` runs the three in order and
stops at the first failure.
```sh
myos up # the current directory, when it holds a compose file
myos up host # a group: expands to host/consul host/fabio ...
myos up host/fabio # one stack
myos up postgres:9.6 # the 9.6 overlay of the postgres stack
myos -C /opt/app up # somewhere else
myos -e master up # in another environment
```
Stacks are looked up along the stack path: `./stack`, `../stack`,
`~/.local/share/myos/stack`, `/usr/local/share/myos/stack`, `/usr/share/myos/stack`.
`myos doctor` prints the resolved path.
A stack found in several of them is **merged**, the project last, so a project
adds `stack/postgres/postgres.local.yml` next to the catalogue's `postgres.yml`
and refines it instead of replacing it. The same goes for the settings hooks.
## Three kinds of stack
| kind | reference | compose project | meaning |
|---|---|---|---|
| host | `host/*` | the hostname | one per machine: binds 80/443, consul, certbot. Shared. Treat as production. |
| user | `User/*` | the user identity | one per user: ssh-agent, personal services |
| app | anything else | `<user>-<env>-<app>` | many per machine, one per user and environment |
Set `MYOS_PROJECT_FORMAT=user-app-env` on a deployment created before the
rename, otherwise its containers and volumes look like a different project.
See `references/conventions.md`.
## Rules
- 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.
- A stack of the catalogue is shared: change it in `myos-stacks`, not in place
on a server.
- Before upgrading myos on a machine that already runs stacks, pin the naming
(`references/conventions.md`), or every project gets a new name.
## More
- `references/commands.md` — every command, and its equivalent in the old make targets
- `references/conventions.md` — file layout, naming, networks, variables, tags
- `references/troubleshooting.md` — what each failure means and how to fix it
- `references/authoring.md` — writing a stack, and working on myos itself
+78
View File
@@ -0,0 +1,78 @@
# Writing a stack, and working on myos
## A new stack in the catalogue
```
stack/<name>/<name>.yml the services
stack/<name>/<name>.local.yml what only makes sense on a workstation (published ports…)
stack/<name>/<name>.labels.yml the registrator labels, so routing stays optional
stack/<name>/<name>.env plain settings: versions, defaults
stack/<name>/<name>.sh lazy defaults (fabio tags), no make needed
stack/<name>/.env.dist the variables it expects, with defaults
stack/<name>/README.md what it is and what it needs
```
Rules that keep a stack reusable:
- no `container_name`, except in a `host/` stack: it prevents scaling and
collides between users,
- no fixed host port outside `host/`; publish through the load balancer,
- reference variables with a default: `${POSTGRES_VERSION:-16}`,
- attach to `private` to be reachable by the other stacks of the user, to
`public` to be routed,
- name volumes, never bind-mount an absolute path.
Check it before committing:
```sh
myos -n config <name> # the file list and the project
myos config <name> # the rendered yaml
```
## A group
```sh
# stack/<group>.env
mygroup=<name> other/<name>
```
Lowercase, and it may name other groups.
## Working on myos itself
```sh
make test # unit + golden, against both engines, with a mocked docker
make test-golden # golden only
make golden-record # re-record the golden expectations from the make engine
make lint # shellcheck
```
Layout:
```
bin/myos argument parsing, configuration, dispatch
lib/core.sh logging, exit codes, dry run
lib/str.sh strings and version comparison
lib/naming.sh project names, networks, user identity
lib/stack.sh stack path, references, overlays, groups
lib/var.sh variable resolution and lazy defaults
lib/config.sh dotenv, templates, variables of the compose files
lib/hooks.sh the per-stack .env and .sh
lib/compose.sh finding and calling docker compose
lib/tags.sh fabio tags
lib/cmd/<x>.sh one file per command
share/compose/ the networks and volumes overlays myos provides
spec/ shellspec
```
Adding a command: write `lib/cmd/<name>.sh` defining `myos_cmd_<name>`, add it
to the usage text in `bin/myos`, and cover it in `spec/unit`.
Constraints:
- POSIX shell, no bashisms: it has to run under the bash 3.2 of macOS and the
ash of Alpine. `make test` runs on both.
- No `a-z` ranges in a `case` pattern: under a dictionary collation such as
`fr_FR` they also match uppercase. Use `[:lower:]`.
- A function that has to return several values takes them out through printf,
not through a global: a caller inside `$( )` would lose the global.
- Changing what a command prints means updating `spec/golden/expected*/` and
explaining the change in `spec/golden/DELTAS.md`.
+90
View File
@@ -0,0 +1,90 @@
# Commands
```
myos [options] <command> [stack...] [VAR=value...] [-- args...]
```
| option | effect |
|---|---|
| `-C DIR` | work in DIR instead of the current directory |
| `-e ENV` | environment: picks `.env.<env>` and the `<stack>.<env>.yml` overlays |
| `-n`, `--dry-run` | print the commands instead of running them |
| `-v`, `--verbose` | say what myos is doing |
| `-d`, `--debug` | print every command |
| command | effect |
|---|---|
| `up` | create and start; creates the external networks first |
| `down` | remove the containers |
| `start` / `stop` / `restart` | on the existing containers |
| `ps` | what is running |
| `logs` | follow the logs |
| `config` | the resolved compose file |
| `exec` | run a command in a running service |
| `run` | run it in a new container, removed afterwards |
| `scale` | `myos scale <stack> SERVICE=<name> NUM=<n>` |
| `build` / `pull` | images |
| `ls [--groups]` | the stacks and groups myos can see |
| `env [VAR...]` | resolved variables |
| `env-update` | fill the `.env` from the `.env.dist` templates |
| `doctor` | check the installation |
| `version` | the myos version |
## Several commands at once
Commands chain, the way make targets did. Leading words that name commands are
commands; the first word that is not one starts the list of stacks.
```sh
myos build up logs host/fabio # like: make build up logs STACK=host/fabio
myos up ps host
```
They run in order and stop at the first failure. A stack whose name is also a
command name has to be given as `STACK=<name>`, otherwise it is read as a
command.
Anything after `--` goes to docker compose:
```sh
myos logs host/fabio -- --tail 20
myos up postgres -- --force-recreate
```
`exec` and `run` take the service from the stack name, since most stacks name
their main service after themselves. `SERVICE=` picks another one:
```sh
myos exec host/consul -- consul members # service consul, command "consul members"
myos exec host/fabio SERVICE=fabio -- sh
myos run postgres -- psql -l
```
## Exit codes
| code | meaning |
|---|---|
| 0 | fine |
| 1 | the command failed |
| 2 | bad invocation, or an unknown command |
| 3 | stack not found (the message prints the search path) |
| 4 | missing requirement, run `myos doctor` |
The make engine exited 0 on an unknown target, printing only a warning. The CLI
does not: a typo is an error.
## Coming from the make targets
| make | myos |
|---|---|
| `make up STACK=host` | `myos up host` |
| `make print-COMPOSE_FILE` | `myos env COMPOSE_FILE` |
| `make host` | `myos up host` (a bare stack name is not a command) |
| `make build up logs STACK=host/fabio` | `myos build up logs host/fabio` |
| `make stack-host-config` | `myos config host` |
| `make up@master` | `myos -e master up` |
| `make exec SERVICE=php ARGS='ls'` | `myos exec <stack> -- php ls` |
| `make DRYRUN=true up` | `myos -n up` |
`print-VAR`, `stack-<stack>-<command>` and `<command>@<env>` still work.
A project `Makefile` that includes `make/include.mk` keeps working too.
+182
View File
@@ -0,0 +1,182 @@
# Conventions
## A stack is a directory of compose files
For stack `<name>` in environment `<env>`, myos loads whichever of these exist,
in this order. A later file overrides the ones before it.
```
<name>.yml the stack
<name>.<env>.yml this environment only
<env>/<name>.yml same, when the stack keeps a directory per environment
<name>.<suffix>.yml an optional overlay, see below
<name>.<suffix>.<env>.yml
<name>.<version>.yml when the reference is <name>:<version>
```
`docker-compose.yml` is read under the same rules, so a stack can keep an
upstream `docker-compose.yml` untouched and add its own `<name>.yml` on top.
Then myos appends its own `share/compose/networks.yml`, and the
`volumes.<suffix>.<env>.yml` of the enabled suffixes.
## Overlay suffixes
Every `COMPOSE_FILE_<X>` variable that is not `false` enables the suffix `<x>`.
| variable | default | loads |
|---|---|---|
| `COMPOSE_FILE_APP` | true | `<name>.app.yml` |
| `COMPOSE_FILE_LABELS` | true | `<name>.labels.yml`, the registrator labels |
| `COMPOSE_FILE_NETWORKS` | true | `<name>.networks.yml` |
| `COMPOSE_FILE_SSH` | true | `<name>.ssh.yml` |
| `COMPOSE_FILE_VOLUMES` | true | `<name>.volumes.yml` |
| `COMPOSE_FILE_WWW`, `_DNS`, `_HOME`, `_LOG`, `_BACKUP` | false | the matching overlay, and the framework bind mount |
A value that is neither `true` nor `false` also enables `<suffix>.<value>`:
`COMPOSE_FILE_WWW=nginx` loads `<name>.www.yml` **and** `<name>.www.nginx.yml`.
## Project names
| scope | project | why |
|---|---|---|
| `host/*` | `$HOSTNAME` | a singleton of the machine: one consul, one fabio on 80/443 |
| `User/*` | the user identity derived from the mail address | one per user |
| `cluster/*` | the stack name | one namespace per swarm |
| anything else | `<user>-<env>-<app>` | one per user, environment and app |
`MYOS_PROJECT_FORMAT=user-app-env` restores the older order. **Any deployment
created before the rename must set it**, in its `.env` or in
`/etc/conf.d/myos`; otherwise the containers and volumes of the old project
are orphaned and the stack comes back up empty.
## Networks
| network | name | scope |
|---|---|---|
| `default` | `_<project>` | the project. The leading underscore keeps it first alphabetically, so it is the first interface attached and service names never resolve across stacks. |
| `private` | `<user>-<env>` | external, shared between the stacks of one user and environment |
| `public` | `<hostname>` | external, where the load balancer reaches the services |
`myos up` creates the external ones when they are missing.
## Routing: registrator, consul, fabio
A service is published by labels, which registrator copies into consul, and on
which fabio routes:
```yaml
labels:
- SERVICE_8000_NAME=${COMPOSE_SERVICE_NAME}-kong-8000
- SERVICE_8000_TAGS=${SUPABASE_KONG_SERVICE_8000_TAGS:-urlprefix-supabase.localhost/*}
- SERVICE_8000_CHECK_TCP=true
- SERVICE_8001_IGNORE=true
```
Registrator only sees ports that are `expose`d, and consul only routes a
service whose check passes.
Tag variables follow `<STACK>_SERVICE_<port>_<KEY>`, with `PATH`, `OPTS`,
`URIS`, `NAME`, and the fabio options `allow`, `auth`, `deny`, `prepend`,
`proto`, `register`, `strip`:
```sh
HOST_NGINX_SERVICE_443_PROTO='https tlsskipverify=true'
DUNITER_V2S_SERVICE_9944_STRIP=/ws
```
## Configuration
Layers, the last one wins:
```
defaults < /etc/conf.d/myos, /etc/default/myos < ~/.config/myos/config
< <workdir>/.env < <workdir>/.env.<env> < environment < VAR=value on the command line
```
All of them are dotenv: `KEY=value`, `#` comments, optional quotes. They are
parsed, never sourced, so a value may contain a `#` or a `$(...)` without
breaking anything or being executed. The make engine included `.env` as a
makefile, where both broke.
## Where a stack lives
The same stack may exist in several directories of the stack path. They are all
loaded, least specific first, so the project has the last word:
```
/usr/local/share/myos/stack/postgres/postgres.yml the catalogue
./stack/postgres/postgres.local.yml the project refines it
./stack/postgres/postgres.sh and may redefine a default
```
Nothing has to be copied to change one setting: a value in the project `.env`
already wins over any default a stack ships.
## Per-stack settings
A stack keeps its own settings next to its compose files:
| file | for |
|---|---|
| `<name>.env` | plain values: versions, defaults |
| `<name>.env.<env>` | the same, for one environment |
| `<name>.sh` | values that have to be computed |
| `<name>.mk` | the legacy make snippet; still read for its groups |
A `.sh` hook declares **lazy defaults**: a function named
`myos_default_<VARIABLE>`, called only when the variable has no value, and
called again at each reference. That is the make `?=` on a recursive variable,
in shell:
```sh
# stack/host/fabio.sh
myos_default_HOST_FABIO_VERSION() { printf '1.6.3'; }
myos_default_HOST_FABIO_SERVICE_9998_NAME() { printf 'fabio'; }
myos_default_HOST_FABIO_SERVICE_9998_AUTH() { printf 'default'; }
myos_default_HOST_FABIO_SERVICE_9998_TAGS() { myos_tagprefix HOST_FABIO 9998; }
```
Two things follow, and they are the point:
- a value given anywhere (environment, `.env`, command line) wins over the
default, without the hook having to say so;
- the default is computed against the values current **at the moment it is
read**, so a `DOMAIN` set in a `.env` loaded later is taken into account.
The prefix is not decoration: a bare function named `host` or `test` would be
indistinguishable from the program of that name, and myos would run it.
Helpers available in a hook: `myos_tagprefix`, `myos_urlprefix`, `myos_uri`,
`myos_url`, `myos_envprefix`, `myos_servicenvs`, `myos_var`, `myos_lower`,
`myos_upper`, and `myos_default NAME 'body'` when the name is built at run time.
## Templates: .env.dist
A stack may ship a `.env.dist` listing the variables it expects, with their
defaults. `myos env-update` writes the missing ones into the `.env`, expanding
`${VAR}` against the current values and running `$(command)`:
```sh
# stack/demo/.env.dist
DEMO_IMAGE=alpine:${DEMO_VERSION}
DEMO_VERSION=3.20
DEMO_SECRET=$(openssl rand -hex 16)
```
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.
## Groups
A group is a lowercase name whose value lists stacks. It can live in a `.env`,
in the environment, in `<path>/<group>.env`, or in a legacy `<group>.mk`:
```sh
host=host/consul host/fabio host/registrator
testing=drone/drone drone/gc
```
Groups expand recursively. Only lowercase names are considered, so an
environment variable can never be mistaken for a group.
+64
View File
@@ -0,0 +1,64 @@
# Troubleshooting
Start with `myos doctor`, then `myos -n <command>` to see what would run.
## `stack not found: <name>` (exit 3)
The reference is not on the stack path, which the message prints.
- `myos ls` shows what is reachable.
- The catalogue may not be installed: `git clone <myos-stacks> /usr/local/share/myos`.
- A stack of the project is only found from the project: use `myos -C /path/to/project`.
## `docker compose >= 2.24.4 not found` (exit 4)
Install the compose plugin, or a `docker-compose` binary of that version.
myos no longer falls back to a compose image.
## `unknown command: <x>` (exit 2)
Check the spelling against `myos help`. The make engine accepted any target
and exited 0 after printing a warning, so typos used to look like successes.
## The containers came back under a different name
The default project name changed from `<user>-<app>-<env>` to
`<user>-<env>-<app>`. The old containers and volumes are still there, under the
old project. Put `MYOS_PROJECT_FORMAT=user-app-env` in the `.env` of the
deployment (or in `/etc/conf.d/myos`) and bring it up again.
Check first: `myos env COMPOSE_PROJECT_NAME` against `docker ps --format '{{.Names}}'`.
## `network <name> declared as external, but could not be found`
The `private` or `public` network is missing. `myos up` creates them; a bare
`docker compose up` does not. Or create it by hand:
`docker network create <user>-<env>`.
## A service is up but not routed
In order: the port must be `expose`d (registrator ignores what it cannot see),
the labels must be on the service, the consul check must pass, and only then
does fabio route the `urlprefix-` tag.
```sh
myos config <stack> | grep -A5 labels # what the labels resolve to
myos exec host/consul -- consul catalog services
myos logs host/registrator
```
## A variable is empty in the container
myos only passes the variables the compose files actually mention. Check with
`myos env` and `myos config <stack>`. A variable set in a `.env` of another
directory is not read: only the workdir's `.env` is.
## On macOS with Colima
The daemon lives in a VM: a bind mount only works for a path the VM shares, and
`host.docker.internal` is the way back to the host. `myos doctor` prints the
`DOCKER_HOST` in use.
## Something changed after upgrading myos
`spec/golden/DELTAS.md` in the myos repository lists every intentional
difference between the make engine and the CLI, with the reason.
+2
View File
@@ -0,0 +1,2 @@
MYOS ?= /usr/local/lib/myos
include $(MYOS)/share/make/shim.mk
+12
View File
@@ -0,0 +1,12 @@
services:
consul:
image: hashicorp/consul:1.15
container_name: ${HOST_COMPOSE_PROJECT_NAME:-localhost}-consul
network_mode: host
restart: always
environment:
CONSUL_HTTP_TOKEN: ${HOST_CONSUL_HTTP_TOKEN}
volumes:
- consul:/consul/data
volumes:
consul:
+13
View File
@@ -0,0 +1,13 @@
services:
fabio:
image: fabiolb/fabio:1.6.3
container_name: ${HOST_COMPOSE_PROJECT_NAME:-localhost}-fabio
depends_on: [consul]
ports:
- "80:80"
- "443:443"
environment:
FABIO_REGISTRY_CONSUL_ADDR: ${DOCKER_HOST_INET4:-127.0.0.1}:8500
FABIO_LOG_ACCESS_TARGET: ${HOST_FABIO_LOG_ACCESS:-}
networks:
- public
+7
View File
@@ -0,0 +1,7 @@
host ?= host/consul host/fabio
# a target of the project, on top of the myos commands: this is what make is
# kept for, and what the shim leaves alone
.PHONY: host-certs
host-certs:
@echo "would renew the certificates of $(host)"
+9
View File
@@ -0,0 +1,9 @@
services:
registrator:
image: gliderlabs/registrator:master
container_name: ${HOST_COMPOSE_PROJECT_NAME:-localhost}-registrator
network_mode: host
depends_on: [consul]
command: -internal=false -useIpFromLabel SERVICE_ADDRESS consul://127.0.0.1:8500
volumes:
- ${DOCKER_SOCKET_LOCATION:-/var/run/docker.sock}:/tmp/docker.sock
+14
View File
@@ -70,3 +70,17 @@ inc-app-print-env-vars | app-git | @make print-ENV_VARS
inc-app-print-network-default | app-git | @make print-DOCKER_NETWORK_DEFAULT inc-app-print-network-default | app-git | @make print-DOCKER_NETWORK_DEFAULT
inc-app-stack-postgres-up | app-git | @make up STACK=postgres inc-app-stack-postgres-up | app-git | @make up STACK=postgres
inc-app-unknown-target | app-git | @make doesnotexist inc-app-unknown-target | app-git | @make doesnotexist
cmd-exec-host | host-project | exec host/consul -- consul members
cmd-exec-service | host-project | exec host/fabio SERVICE=fabio -- sh
cmd-run-catalogue | app-nogit | run postgres -- psql -l
cmd-scale | app-nogit | scale postgres SERVICE=postgres NUM=3
cmd-ls | host-project | ls
cmd-ls-groups | host-project | ls --groups
cmd-env-domain | host-project | env DOMAIN
shim-up-group | shim-project | @make up STACK=host DRYRUN=true
shim-config | shim-project | @make config STACK=host/consul DRYRUN=true
shim-project-target | shim-project | @make host-certs
shim-env | shim-project | @make env ARGS=COMPOSE_PROJECT_NAME STACK=host
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
+2 -26
View File
@@ -1,26 +1,2 @@
ERROR: unknown command: run docker compose -f @WD@/docker-compose.yml -f @WD@/docker-compose.local.yml -f @WD@/docker/docker-compose.yml -f @MYOS@/share/compose/networks.yml -p tester-wd-local --project-directory @WD@ run --rm app
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...] [exit 0]
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
-v, --verbose show what myos does
-d, --debug show every command
-h, --help this help
Commands:
up down start stop restart recreate manage the containers of a stack
ps logs config exec run inspect and enter them
ls [--groups] list the stacks myos can see
env [VAR...] show resolved variables
doctor check the installation
version print the myos version
Stacks:
myos up the stack of the current directory
myos up host a group, expanded from host=... in a .env or .mk
myos up host/fabio a single stack
myos up postgres:9.6 a versioned stack
[exit 2]
+4 -26
View File
@@ -1,26 +1,4 @@
ERROR: unknown command: scale docker network create tester-local
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...] docker network create testhost
docker compose -f @WD@/docker-compose.yml -f @WD@/docker-compose.local.yml -f @WD@/docker/docker-compose.yml -f @MYOS@/share/compose/networks.yml -p tester-wd-local --project-directory @WD@ up -d --scale app=2
Options: [exit 0]
-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
-v, --verbose show what myos does
-d, --debug show every command
-h, --help this help
Commands:
up down start stop restart recreate manage the containers of a stack
ps logs config exec run inspect and enter them
ls [--groups] list the stacks myos can see
env [VAR...] show resolved variables
doctor check the installation
version print the myos version
Stacks:
myos up the stack of the current directory
myos up host a group, expanded from host=... in a .env or .mk
myos up host/fabio a single stack
myos up postgres:9.6 a versioned stack
[exit 2]
@@ -1,26 +1,3 @@
ERROR: unknown command: doesnotexist ERROR: unknown command: doesnotexist
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...] ERROR: to act on a stack of that name, say what to do: myos up doesnotexist
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
-v, --verbose show what myos does
-d, --debug show every command
-h, --help this help
Commands:
up down start stop restart recreate manage the containers of a stack
ps logs config exec run inspect and enter them
ls [--groups] list the stacks myos can see
env [VAR...] show resolved variables
doctor check the installation
version print the myos version
Stacks:
myos up the stack of the current directory
myos up host a group, expanded from host=... in a .env or .mk
myos up host/fabio a single stack
myos up postgres:9.6 a versioned stack
[exit 2] [exit 2]
@@ -0,0 +1,6 @@
docker compose -f @WD@/stack/host/fabio.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host build
docker network create tester-local
docker network create testhost
docker compose -f @WD@/stack/host/fabio.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host up -d
docker compose -f @WD@/stack/host/fabio.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host logs --follow --tail=100
[exit 0]
@@ -0,0 +1,5 @@
COMPOSE_PROJECT_NAME testhost
APP consul
COMPOSE_PROJECT_NAME testhost
APP consul
[exit 0]
@@ -0,0 +1,5 @@
docker network create tester-local
docker network create testhost
docker compose -f @WD@/stack/host/consul.yml -f @WD@/stack/host/fabio.yml -f @WD@/stack/host/registrator.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host up -d
docker compose -f @WD@/stack/host/consul.yml -f @WD@/stack/host/fabio.yml -f @WD@/stack/host/registrator.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host ps
[exit 0]
@@ -0,0 +1,2 @@
DOMAIN example.test
[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 exec consul consul members
[exit 0]
@@ -0,0 +1,2 @@
docker compose -f @WD@/stack/host/fabio.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host exec fabio sh
[exit 0]
@@ -0,0 +1,4 @@
host host/consul host/fabio host/registrator
default postgres redis
testing drone/drone redis
[exit 0]
+9
View File
@@ -0,0 +1,9 @@
@WD@/stack
host 3 compose file(s)
@HOME@/.local/share/myos/stack
User 1 compose file(s)
drone 2 compose file(s)
host 3 compose file(s)
postgres 3 compose file(s)
redis 1 compose file(s)
[exit 0]
@@ -0,0 +1,2 @@
docker compose -f @HOME@/.local/share/myos/stack/postgres/postgres.yml -f @HOME@/.local/share/myos/stack/postgres/postgres.local.yml -f @MYOS@/share/compose/networks.yml -p tester-postgres-local --project-directory @HOME@/.local/share/myos/stack/postgres run --rm postgres psql -l
[exit 0]
+4
View File
@@ -0,0 +1,4 @@
docker network create tester-local
docker network create testhost
docker compose -f @HOME@/.local/share/myos/stack/postgres/postgres.yml -f @HOME@/.local/share/myos/stack/postgres/postgres.local.yml -f @MYOS@/share/compose/networks.yml -p tester-postgres-local --project-directory @HOME@/.local/share/myos/stack/postgres up -d --scale postgres=3
[exit 0]
+2 -26
View File
@@ -1,26 +1,2 @@
ERROR: unknown command: exec docker compose -f @WD@/stack/host/consul.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host exec consul
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...] [exit 0]
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
-v, --verbose show what myos does
-d, --debug show every command
-h, --help this help
Commands:
up down start stop restart recreate manage the containers of a stack
ps logs config exec run inspect and enter them
ls [--groups] list the stacks myos can see
env [VAR...] show resolved variables
doctor check the installation
version print the myos version
Stacks:
myos up the stack of the current directory
myos up host a group, expanded from host=... in a .env or .mk
myos up host/fabio a single stack
myos up postgres:9.6 a versioned stack
[exit 2]
+2 -1
View File
@@ -1,2 +1,3 @@
ERROR: no stack given, and no compose file in @WD@ ERROR: unknown command: host
ERROR: to act on a stack of that name, say what to do: myos up host
[exit 2] [exit 2]
@@ -0,0 +1,10 @@
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=@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
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local up -d
make -o docker-stack-logs MAKE_OLDFILE=docker-stack-logs ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-logs STACK=myos APP_NAME=myos
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local logs --follow --tail=100
WARNING: myos[0] host/fabio-rule-exists: target host/fabio unavailable in app myos
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
COMPOSE_PROJECT_NAME testhost
APP myos
[exit 0]
+20
View File
@@ -0,0 +1,20 @@
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
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local up -d
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
make -o host MAKE_OLDFILE=host ENV=local DOCKER_COMPOSE=docker --log-level=error compose up STACK=host/consul host/fabio host/registrator
make -o host MAKE_OLDFILE=host ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-up STACK=host/consul APP_NAME=host
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 @WD@/stack/host/consul.yml -f @MYOS@/share/compose/networks.yml -p testhost up -d
make -o host MAKE_OLDFILE=host ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-up STACK=host/fabio APP_NAME=host
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 @WD@/stack/host/fabio.yml -f @MYOS@/share/compose/networks.yml -p testhost up -d
make -o host MAKE_OLDFILE=host ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-up STACK=host/registrator APP_NAME=host
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 @WD@/stack/host/registrator.yml -f @MYOS@/share/compose/networks.yml -p testhost up -d
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
WARNING: myos[0] env-rule-exists: target env unavailable in app myos
WARNING: myos[0] DOMAIN-rule-exists: target DOMAIN unavailable in app myos
[exit 0]
+5
View File
@@ -0,0 +1,5 @@
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local exec -T sh -c host/consul consul members
/bin/bash: -c: line 0: syntax error near unexpected token `||'
/bin/bash: -c: line 0: `|| true'
make: *** [exec] Error 2
[exit 2]
@@ -0,0 +1,5 @@
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local exec -T fabio sh -c host/fabio sh
/bin/bash: -c: line 0: syntax error near unexpected token `||'
/bin/bash: -c: line 0: `|| true'
make: *** [exec] Error 2
[exit 2]
+46
View File
@@ -0,0 +1,46 @@
make: unrecognized option `--groups'
Usage: make [options] [target] ...
Options:
-b, -m Ignored for compatibility.
-B, --always-make Unconditionally make all targets.
-C DIRECTORY, --directory=DIRECTORY
Change to DIRECTORY before doing anything.
-d Print lots of debugging information.
--debug[=FLAGS] Print various types of debugging information.
-e, --environment-overrides
Environment variables override makefiles.
-f FILE, --file=FILE, --makefile=FILE
Read FILE as a makefile.
-h, --help Print this message and exit.
-i, --ignore-errors Ignore errors from commands.
-I DIRECTORY, --include-dir=DIRECTORY
Search DIRECTORY for included makefiles.
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
-k, --keep-going Keep going when some targets can't be made.
-l [N], --load-average[=N], --max-load[=N]
Don't start multiple jobs unless load is below N.
-L, --check-symlink-times Use the latest mtime between symlinks and target.
-n, --just-print, --dry-run, --recon
Don't actually run any commands; just print them.
-o FILE, --old-file=FILE, --assume-old=FILE
Consider FILE to be very old and don't remake it.
-p, --print-data-base Print make's internal database.
-q, --question Run no commands; exit status says if up to date.
-r, --no-builtin-rules Disable the built-in implicit rules.
-R, --no-builtin-variables Disable the built-in variable settings.
-s, --silent, --quiet Don't echo commands.
-S, --no-keep-going, --stop
Turns off -k.
-t, --touch Touch targets instead of remaking them.
-v, --version Print the version number of make and exit.
-w, --print-directory Print the current directory.
--no-print-directory Turn off -w, even if it was turned on implicitly.
-W FILE, --what-if=FILE, --new-file=FILE, --assume-new=FILE
Consider FILE to be infinitely new.
--warn-undefined-variables Warn when an undefined variable is referenced.
-N OPTION, --NeXT-option=OPTION
Turn on value of NeXT OPTION.
This program built for i386-apple-darwin11.3.0
Report bugs to <bug-make@gnu.org>
[exit 2]
+2
View File
@@ -0,0 +1,2 @@
WARNING: myos[0] ls-rule-exists: target ls unavailable in app myos
[exit 0]
@@ -0,0 +1,2 @@
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local run --rm postgres psql -l
[exit 0]
+4
View File
@@ -0,0 +1,4 @@
make -o docker-stack-scale MAKE_OLDFILE=docker-stack-scale ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-scale STACK=myos APP_NAME=myos
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local up -d --scale postgres=3
WARNING: myos[0] postgres-rule-exists: target postgres unavailable in app myos
[exit 0]
+2
View File
@@ -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 config
[exit 0]
+2
View File
@@ -0,0 +1,2 @@
COMPOSE_PROJECT_NAME testhost
[exit 0]
@@ -0,0 +1,2 @@
would renew the certificates of host/consul host/fabio
[exit 0]
+4
View File
@@ -0,0 +1,4 @@
docker network create tester-local
docker network create testhost
docker compose -f @WD@/stack/host/consul.yml -f @WD@/stack/host/fabio.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host up -d
[exit 0]
+5 -1
View File
@@ -1,6 +1,10 @@
#!/bin/sh #!/bin/sh
# Mock docker for myos tests: logs every call, answers a few read-only queries deterministically. # Mock docker for myos tests: logs every call, answers a few read-only queries deterministically.
[ -n "${MYOS_DOCKER_LOG:-}" ] && printf 'docker %s\n' "$*" >> "$MYOS_DOCKER_LOG" if [ -n "${MYOS_DOCKER_LOG:-}" ]; then
printf 'docker %s\n' "$*" >> "$MYOS_DOCKER_LOG"
# the environment is recorded too, so a test can assert what was exported
env > "$(dirname "$MYOS_DOCKER_LOG")/env.log"
fi
case "$1 $2" in case "$1 $2" in
"compose version") echo "${MOCK_COMPOSE_VERSION:-2.29.0}" ;; "compose version") echo "${MOCK_COMPOSE_VERSION:-2.29.0}" ;;
"compose config") echo "# mock compose config" ;; "compose config") echo "# mock compose config" ;;
+5 -1
View File
@@ -1,4 +1,8 @@
#!/bin/sh #!/bin/sh
[ -n "${MYOS_DOCKER_LOG:-}" ] && printf 'docker-compose %s\n' "$*" >> "$MYOS_DOCKER_LOG" if [ -n "${MYOS_DOCKER_LOG:-}" ]; then
printf 'docker-compose %s\n' "$*" >> "$MYOS_DOCKER_LOG"
# the environment is recorded too, so a test can assert what was exported
env > "$(dirname "$MYOS_DOCKER_LOG")/env.log"
fi
case "$1" in version) echo "${MOCK_COMPOSE_VERSION:-2.29.0}" ;; config) echo "# mock compose config" ;; *) : ;; esac case "$1" in version) echo "${MOCK_COMPOSE_VERSION:-2.29.0}" ;; config) echo "# mock compose config" ;; *) : ;; esac
exit 0 exit 0
+5 -2
View File
@@ -39,6 +39,8 @@ myos_normalize() {
-e 's/^APPS .*/APPS @APPS@/' \ -e 's/^APPS .*/APPS @APPS@/' \
-e 's/^BRANCH .*/BRANCH @BRANCH@/' \ -e 's/^BRANCH .*/BRANCH @BRANCH@/' \
-e 's/^VERSION .*/VERSION @VERSION@/' \ -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:]][[:space:]]*/ /g' \
-e 's/[[:space:]]*$//' -e 's/[[:space:]]*$//'
} }
@@ -47,8 +49,9 @@ myos_normalize() {
# shellcheck disable=SC2046 # myos_hermetic_env output is meant to be word-split # shellcheck disable=SC2046 # myos_hermetic_env output is meant to be word-split
myos_run_engine() { myos_run_engine() {
_engine=$1; _sb=$2; shift 2 _engine=$1; _sb=$2; shift 2
# "@make" as first arg = include mode: the project Makefile includes make/include.mk # "@make" as first arg = the project drives make itself: its Makefile includes
# and make runs from the project dir (CURDIR = project). Same for both engines. # either the legacy engine (make/include.mk) or the shim (make/shim.mk), and
# make runs from the project directory.
if [ "${1:-}" = "@make" ]; then if [ "${1:-}" = "@make" ]; then
shift shift
_out=$(cd "$_sb/wd" && env -i $(myos_hermetic_env "$_sb") MYOS_CONF=/dev/null \ _out=$(cd "$_sb/wd" && env -i $(myos_hermetic_env "$_sb") MYOS_CONF=/dev/null \
+76
View File
@@ -0,0 +1,76 @@
#shellcheck shell=sh
Include lib/str.sh
Include lib/core.sh
Include lib/var.sh
Include lib/tags.sh
Include lib/config.sh
Include lib/compose.sh
# These assertions look at what is actually executed, not at what --dry-run
# prints: the two used to disagree, because the execution path set IFS to a
# newline and so never split "-f a -f b", nor the two words "docker compose".
Describe 'lib/compose.sh execution'
setup() {
MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-compose.XXXXXX")
MYOS_TMP=$(cd "$MYOS_TMP" && pwd -P)
printf 'services:\n a:\n image: ${IMAGE}\n' > "$MYOS_TMP/a.yml"
printf 'services:\n b:\n image: alpine\n' > "$MYOS_TMP/b.yml"
# exported, otherwise the mock (a child process) never sees it
export MYOS_DOCKER_LOG=$MYOS_TMP/docker.log
export PATH=$SPEC_DIR/support/bin:$PATH
DRYRUN=false
IMAGE=alpine:3.20
unset MYOS_COMPOSE_BIN 2>/dev/null || true
}
cleanup() { rm -rf "$MYOS_TMP"; }
BeforeEach setup
AfterEach cleanup
logged() { cat "$MYOS_DOCKER_LOG"; }
two_files() { myos_compose demo "$(printf '%s\n%s' "$MYOS_TMP/a.yml" "$MYOS_TMP/b.yml")" -- config; }
It 'passes every -f as its own argument'
When call two_files
The status should be success
The result of function logged should include "-f $MYOS_TMP/a.yml -f $MYOS_TMP/b.yml"
The result of function logged should include "-p demo"
The result of function logged should include "--project-directory $MYOS_TMP"
The result of function logged should end with "config"
End
It 'splits the two words of the docker compose plugin'
MYOS_COMPOSE_BIN="docker compose"
When call myos_compose demo "$MYOS_TMP/a.yml" -- config
The status should be success
The result of function logged should start with "docker compose --ansi=auto"
End
It 'runs the docker-compose binary as one word'
MYOS_COMPOSE_BIN="docker-compose"
When call myos_compose demo "$MYOS_TMP/a.yml" -- config
The status should be success
The result of function logged should start with "docker-compose --ansi=auto"
End
It 'passes the variables the compose files reference'
MYOS_COMPOSE_BIN="docker-compose"
When call myos_compose demo "$MYOS_TMP/a.yml" -- config
The status should be success
The contents of file "$MYOS_TMP/env.log" should include "IMAGE=alpine:3.20"
End
It 'keeps a value that contains spaces in one piece'
MYOS_COMPOSE_BIN="docker-compose"
IMAGE="alpine with spaces"
When call myos_compose demo "$MYOS_TMP/a.yml" -- config
The status should be success
The contents of file "$MYOS_TMP/env.log" should include "IMAGE=alpine with spaces"
End
It 'refuses to run without a compose file'
When call myos_compose demo "" -- config
The status should equal 3
The stderr should include "no compose file"
End
End
+93
View File
@@ -1,6 +1,7 @@
#shellcheck shell=sh #shellcheck shell=sh
Include lib/str.sh Include lib/str.sh
Include lib/core.sh Include lib/core.sh
Include lib/var.sh
Include lib/tags.sh Include lib/tags.sh
Include lib/config.sh Include lib/config.sh
@@ -104,3 +105,95 @@ Describe 'lib/config.sh robustness'
The status should be success The status should be success
End End
End End
# The make engine generated a .env out of a .env.dist, expanding ${VAR} against
# the current values and running $(cmd). These check the shell equivalent.
Describe 'lib/config.sh templates'
setup() { MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-tpl.XXXXXX"); }
cleanup() { rm -rf "$MYOS_TMP"; }
BeforeEach setup
AfterEach cleanup
Describe 'myos_expand'
It 'substitutes a variable'
DOMAIN=example.org
When call myos_expand 'https://app.${DOMAIN}/'
The output should equal "https://app.example.org/"
End
It 'substitutes several, including twice the same'
DOMAIN=example.org
When call myos_expand '${DOMAIN}:${DOMAIN}'
The output should equal "example.org:example.org"
End
It 'substitutes a lazy default like any other value'
# shellcheck disable=SC2317
myos_default_LAZY_DOMAIN() { printf 'lazy.example.org'; }
When call myos_expand 'https://${LAZY_DOMAIN}/'
The output should equal "https://lazy.example.org/"
End
It 'empties an unknown variable, as make does'
When call myos_expand 'a${NOT_SET_ANYWHERE}b'
The output should equal "ab"
End
It 'runs a command substitution'
When call myos_expand 'pre-$(echo mid)-post'
The output should equal "pre-mid-post"
End
It 'leaves a malformed reference alone'
When call myos_expand 'a${not-a-name}b'
The output should equal 'a${not-a-name}b'
End
End
Describe 'myos_env_update'
It 'adds the missing variables, expanded'
printf 'DOMAIN=example.org\nAPP_URL=https://app.${DOMAIN}/\n' > "$MYOS_TMP/.env.dist"
DOMAIN=chosen.org
When call myos_env_update "$MYOS_TMP/.env" "$MYOS_TMP/.env.dist"
The status should be success
The contents of file "$MYOS_TMP/.env" should include "APP_URL=https://app.chosen.org/"
The contents of file "$MYOS_TMP/.env" should include "DOMAIN=chosen.org"
End
It 'never touches a value already recorded'
printf 'KEEP=default\n' > "$MYOS_TMP/.env.dist"
printf 'KEEP=already-chosen\n' > "$MYOS_TMP/.env"
When call myos_env_update "$MYOS_TMP/.env" "$MYOS_TMP/.env.dist"
The contents of file "$MYOS_TMP/.env" should equal "KEEP=already-chosen"
End
It 'is idempotent'
printf 'A=1\nB=${A}2\n' > "$MYOS_TMP/.env.dist"
When run source spec/unit/config_update_helper.sh "$MYOS_TMP"
The output should equal "2"
End
It 'does nothing without a template'
When call myos_env_update "$MYOS_TMP/.env" "$MYOS_TMP/nope.dist"
The status should be success
The path "$MYOS_TMP/.env" should not be exist
End
End
End
Describe 'lib/config.sh forward references'
setup() { MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-fwd.XXXXXX"); }
cleanup() { rm -rf "$MYOS_TMP"; }
BeforeEach setup
AfterEach cleanup
It 'resolves a reference to a variable defined further down the template'
printf 'IMAGE=alpine:${VERSION}\nVERSION=3.20\n' > "$MYOS_TMP/.env.dist"
When call myos_env_update "$MYOS_TMP/.env" "$MYOS_TMP/.env.dist"
The contents of file "$MYOS_TMP/.env" should include "IMAGE=alpine:3.20"
End
It 'resolves a chain of references'
printf 'A=${B}\nB=${C}\nC=deep\n' > "$MYOS_TMP/.env.dist"
When call myos_env_update "$MYOS_TMP/.env" "$MYOS_TMP/.env.dist"
The contents of file "$MYOS_TMP/.env" should include "A=deep"
End
It 'still lets an explicit choice win over the template'
printf 'IMAGE=alpine:${VERSION}\nVERSION=3.20\n' > "$MYOS_TMP/.env.dist"
VERSION=3.19
When call myos_env_update "$MYOS_TMP/.env" "$MYOS_TMP/.env.dist"
The contents of file "$MYOS_TMP/.env" should include "IMAGE=alpine:3.19"
The contents of file "$MYOS_TMP/.env" should include "VERSION=3.19"
End
End
+5
View File
@@ -0,0 +1,5 @@
#shellcheck shell=sh
# Running the update twice must leave the same file: print how many lines it has.
myos_env_update "$1/.env" "$1/.env.dist" >/dev/null 2>&1
myos_env_update "$1/.env" "$1/.env.dist" >/dev/null 2>&1
wc -l < "$1/.env" | tr -d ' '
+5
View File
@@ -0,0 +1,5 @@
#shellcheck shell=sh
# Helper: hooks set variables in the caller's scope, which a subshell loses.
[ -n "${4:-}" ] && eval "$3=\$4"
myos_stack_hooks "$1" "$2"
eval "printf '%s\n' \"\${$3:-}\""
+85
View File
@@ -0,0 +1,85 @@
#shellcheck shell=sh
Include lib/str.sh
Include lib/core.sh
Include lib/var.sh
Include lib/tags.sh
Include lib/naming.sh
Include lib/config.sh
Include lib/stack.sh
Include lib/hooks.sh
# A stack ships its computed settings as a shell hook, so the catalogue works
# on a machine without make. These check the hook and the uri it builds on.
Describe 'lib/hooks.sh'
setup() {
MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-hook.XXXXXX")
MYOS_TMP=$(cd "$MYOS_TMP" && pwd -P)
ENV=local; DOMAIN=example.org; USER=tester; HOSTNAME=testhost
MYOS_PATH=$MYOS_TMP
APP_HOST=demo.example.org; APP_URI=demo.example.org/
}
cleanup() { rm -rf "$MYOS_TMP"; }
BeforeEach setup
AfterEach cleanup
It 'loads plain values from a .env hook'
printf 'DEMO_VERSION=1.2.3\n' > "$MYOS_TMP/demo.env"
When run source spec/unit/hooks_helper.sh "$MYOS_TMP" demo DEMO_VERSION
The output should equal "1.2.3"
End
It 'loads a computed value from a .sh hook'
printf 'DEMO_TAGS=$(myos_tagprefix demo 8000)\n' > "$MYOS_TMP/demo.sh"
When run source spec/unit/hooks_helper.sh "$MYOS_TMP" demo DEMO_TAGS
The output should equal "urlprefix-demo.demo.example.org/*"
End
It 'lets a hook name the service it publishes'
printf 'DEMO_SERVICE_8000_NAME=www\nDEMO_TAGS=$(myos_tagprefix demo 8000)\n' > "$MYOS_TMP/demo.sh"
When run source spec/unit/hooks_helper.sh "$MYOS_TMP" demo DEMO_TAGS
The output should equal "urlprefix-www.demo.example.org/*"
End
It 'lets the environment win over a hook default'
printf 'DEMO_VERSION=${DEMO_VERSION:-1.2.3}\n' > "$MYOS_TMP/demo.sh"
When run source spec/unit/hooks_helper.sh "$MYOS_TMP" demo DEMO_VERSION 9.9.9
The output should equal "9.9.9"
End
It 'is quiet when a stack has no hook'
When call myos_stack_hooks "$MYOS_TMP" nothing
The status should be success
The output should equal ""
End
End
Describe 'lib/naming.sh service uris'
BeforeEach 'ENV=local; unset APP_HOST_MULTI_APP APP_HOST_MULTI_USER APP_HOST_MULTI_ENV HOST_LB 2>/dev/null || true'
It 'serves a host stack on <hostname>.<domain>'
When call myos_app_host host aya local fabio example.org sonic
The output should equal "sonic.example.org"
End
It 'adds the bare domain when the host is the load balancer'
HOST_LB=true
When call myos_app_host host aya local fabio example.org sonic
The output should equal "sonic.example.org example.org"
End
It 'does not prefix a main environment'
When call myos_app_host app aya master duniter example.org sonic
The output should equal "example.org"
End
It 'prefixes any other environment'
When call myos_app_host app aya staging duniter example.org sonic
The output should equal "staging.example.org"
End
It 'prefixes the user when asked to'
APP_HOST_MULTI_USER=true
When call myos_app_host app aya master duniter example.org sonic
The output should equal "aya.example.org"
End
It 'builds a uri that ends with a slash'
When call myos_app_uri "a.example.org b.example.org"
The output should equal "a.example.org/ b.example.org/"
End
End
+23
View File
@@ -1,6 +1,7 @@
#shellcheck shell=sh #shellcheck shell=sh
Include lib/str.sh Include lib/str.sh
Include lib/core.sh Include lib/core.sh
Include lib/var.sh
Include lib/tags.sh Include lib/tags.sh
Include lib/stack.sh Include lib/stack.sh
@@ -168,3 +169,25 @@ Describe 'lib/stack.sh group safety'
The lines of output should equal 2 The lines of output should equal 2
End End
End End
Describe 'lib/stack.sh installation prefix'
setup() {
MYOS_TMP=$(mktemp -d "${TMPDIR:-/tmp}/myos-prefix.XXXXXX")
# myos resolves stack paths physically, so compare against the physical path
MYOS_TMP=$(cd "$MYOS_TMP" && pwd -P)
mkdir -p "$MYOS_TMP/lib/myos" "$MYOS_TMP/share/myos/stack/demo"
: > "$MYOS_TMP/share/myos/stack/demo/demo.yml"
MYOS_ROOT=$MYOS_TMP/lib/myos
WORKDIR=$MYOS_TMP
HOME=$MYOS_TMP/nohome
MYOS_PATH=
}
cleanup() { rm -rf "$MYOS_TMP"; }
BeforeEach setup
AfterEach cleanup
It 'finds the catalogue installed beside the framework'
When call myos_stack_resolve demo
The output should equal "$MYOS_TMP/share/myos/stack/demo"
End
End
+52
View File
@@ -68,3 +68,55 @@ Describe 'lib/str.sh'
End End
End 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
+1
View File
@@ -1,6 +1,7 @@
#shellcheck shell=sh #shellcheck shell=sh
Include lib/str.sh Include lib/str.sh
Include lib/core.sh Include lib/core.sh
Include lib/var.sh
Include lib/tags.sh Include lib/tags.sh
# The expectations below are the very examples left as comments in # The expectations below are the very examples left as comments in
+3
View File
@@ -0,0 +1,3 @@
#shellcheck shell=sh
myos_default BUILT 'printf "built-%s" here'
myos_var BUILT
+7
View File
@@ -0,0 +1,7 @@
#shellcheck shell=sh
# A lazy default is re-evaluated at each reference, so it follows a value that
# changes later; an explicit value still wins over it.
myos_default_LAZY_ONE() { printf 'from-%s' "$(myos_var LAZY_BASE)"; }
LAZY_BASE=first; myos_var LAZY_ONE; echo
LAZY_BASE=second; myos_var LAZY_ONE; echo
LAZY_ONE=pinned; myos_var LAZY_ONE; echo
+3
View File
@@ -0,0 +1,3 @@
#shellcheck shell=sh
myos_default_LOOPY() { myos_var LOOPY; }
myos_var LOOPY
+73
View File
@@ -0,0 +1,73 @@
#shellcheck shell=sh
Include lib/core.sh
Include lib/var.sh
Describe 'lib/var.sh'
Describe 'myos_var'
It 'returns the value of a plain variable'
SOME_VAR=plain
When call myos_var SOME_VAR
The output should equal "plain"
End
It 'returns empty for an unset variable'
When call myos_var NEVER_SET
The output should equal ""
End
It 'returns empty for an empty name'
When call myos_var ""
The output should equal ""
End
It 'never runs a command that happens to share the name'
# `host` is a real program: a lazy default must never be confused with it
When call myos_var host
The output should equal ""
The status should be success
The stderr should equal ""
End
End
Describe 'lazy defaults'
lazy() {
myos_default_LAZY_ONE() { printf 'from-%s' "$(myos_var LAZY_BASE)"; }
myos_var LAZY_ONE
}
It 'computes the default when the variable has no value'
LAZY_BASE=a
When call lazy
The output should equal "from-a"
End
It 'recomputes it against the current values'
When run source spec/unit/var_lazy_helper.sh
The line 1 should equal "from-first"
The line 2 should equal "from-second"
The line 3 should equal "pinned"
End
It 'declares a default from a string too'
When run source spec/unit/var_default_helper.sh
The output should equal "built-here"
End
It 'refuses a default written in terms of itself'
When run source spec/unit/var_loop_helper.sh
The stderr should include "defined in terms of itself"
The status should equal 1
End
End
Describe 'myos_var_is_lazy'
It 'is true for a variable that only has a default'
myos_default_ONLY_LAZY() { echo x; }
When call myos_var_is_lazy ONLY_LAZY
The status should be success
End
It 'is false once the variable has a value'
myos_default_BOTH() { echo x; }
BOTH=explicit
When call myos_var_is_lazy BOTH
The status should be failure
End
It 'is false for a program on PATH'
When call myos_var_is_lazy host
The status should be failure
End
End
End