Compare commits

..
11 Commits
Author SHA1 Message Date
Yann Autissier 653a3c9415 harden the config reader against an empty or malformed .env
holcommon's empty .env made the loader evaluate one blank line, which
tripped set -u. Verified afterwards on holcommon's real host stack: the CLI
and the make engine resolve the same five files and render a byte-identical
241-line compose config.
2026-09-03 18:40:31 +02:00
Yann Autissier 35999574bd run the golden suite against the CLI too, and pin every delta
Both engines now go through the same 68 cases. 31 produce byte-identical
output; the rest have a recorded CLI expectation and a reason in DELTAS.md,
the bulk of it being that the CLI does not shell out to a recursive make and
calls compose once per project.

Fixed while comparing: the CLI was missing the COMPOSE_FILE_* defaults, so
overlays such as supabase.labels.yml would not have been loaded at all.
2026-09-03 18:39:23 +02:00
Yann Autissier a346d4f4e1 add the myos CLI: one model for both modes
A stack is a directory of compose files; the current directory is a stack when
it holds one. The same command works for a project, a catalogue stack, a group
and a host singleton, and stacks sharing a project are now a single compose
call.

Two traps of the make engine are closed on the way: an unknown command is an
error instead of a silent success, and a group is only expanded when its name
is lowercase, so an environment variable can no longer be mistaken for one.
That guard spells out its character class because a-z matches uppercase too
under a dictionary collation.
2026-09-03 18:33:47 +02:00
Yann Autissier 990b99f0c0 add the config and compose modules
config: dotenv files are parsed, never sourced, so a value can hold a # or a
$(...) without breaking or executing. The make engine included .env with
make syntax, which neither allowed.
compose: one call per project instead of one per sub-stack.
2026-09-03 18:29:36 +02:00
Yann Autissier e7eab505e9 add stack resolution: search path, references, overlays, groups
Replaces the 500-character stack_path expression of
make/apps/def.docker.mk:159 and its leaked $(stackz) loop variable with
three readable functions, covered by 20 assertions.
2026-09-03 18:27:29 +02:00
Yann Autissier 3437b58078 annotate the cross-file variables for shellcheck 2026-09-03 18:26:14 +02:00
Yann Autissier 5b4db64114 add the pure bash core: core, str, naming and tags
Ported from make/utils.mk, make/def.mk, make/apps/def.docker.mk and
make/apps/def.mk, with the examples that sat as dead comments in
make/apps/def.mk turned into actual assertions.

Two deviations from the make macros are pinned and documented in DELTAS.md;
both only show up on code paths nothing uses.
2026-09-03 18:26:00 +02:00
Yann Autissier 6c97f99f87 run the whole spec dir by default, gate docker-backed specs on a variable
shellspec --tag ~integration filtered out untagged examples too, which
silently emptied the suite.
2026-09-03 18:22:40 +02:00
Yann Autissier 3a64c47260 move the stack catalogue out of the framework
stack/ and docker/ now live in the myos-stacks project (extracted with their
history). What the framework itself needs stays here:
- stack/myos/*.yml            -> share/compose/
- docker/myos/                -> share/docker/myos/
- docker/compose/             -> dropped, docker compose >= 2.24.4 is now required

Residues fixed along the way: DOCKER_IMAGES scanned a hardcoded ./docker,
include.mk filtered a hardcoded stack/*.mk, and docker-image-myos expanded an
undefined MYOS_DOCKER_IMAGES on every up/build.

README and CHANGELOG rewritten: they still documented make host,
host-certbot-* and user-config, all removed when the catalogue was split out.
2026-09-03 18:21:21 +02:00
Yann Autissier 1d44a2ff32 add shellspec golden harness for the legacy make engine
- spec/golden/cases.txt: 69 black-box cases (CLI wrapper mode and make
  include mode) run in a hermetic sandbox with a mocked docker
- spec/golden/expected/*.txt recorded from this engine (baseline)
- make test / test-golden / golden-record / lint dev targets
2026-09-03 14:35:58 +02:00
Yann Autissier e2e34d813e fix stack_path: keep realpath inside each $(or) branch
181a3e9 wrapped both $(or) alternatives in a single $(realpath …), which
turned the argument separator into a literal comma glued to the last path.
host/<svc> stacks were only resolved when the project stack dir happened to
sort before the other STACK_DIR entries.
2026-09-03 14:35:57 +02:00
180 changed files with 2471 additions and 206 deletions
+4
View File
@@ -0,0 +1,4 @@
--require spec_helper
--shell bash
--default-path spec
--pattern "*_spec.sh"
+9
View File
@@ -1,5 +1,14 @@
# CHANGELOG
## v1.1 - 2026-09-03
- move the stack catalogue and the docker build contexts to the myos-stacks project
- keep the framework infra compose files in share/compose and the myos tool image in share/docker
- fix stack_path resolution: host/<svc> stacks were only found when the project
stack directory sorted first
- drop the docker/compose image fallback: docker compose >= 2.24.4 or docker-compose is required
- add a shellspec golden test harness (make test)
## v1.0-beta - 2026-07-29
* split make files in `myos` project and docker files in `stack` project
+23
View File
@@ -1 +1,24 @@
DEV_TARGETS := test test-unit test-golden test-integration lint golden-record
ifneq ($(filter $(DEV_TARGETS),$(MAKECMDGOALS)),)
SHELLSPEC ?= shellspec
SHELLCHECK ?= shellcheck
.PHONY: $(DEV_TARGETS)
test: ## Run unit + golden tests against both engines
$(SHELLSPEC)
MYOS_ENGINE=cli $(SHELLSPEC) spec/golden
test-unit: ## Run unit tests only
$(SHELLSPEC) spec/unit
test-golden: ## Run golden tests only (MYOS_ENGINE=legacy|cli)
$(SHELLSPEC) spec/golden
test-integration: ## Run tests needing a real docker daemon
MYOS_INTEGRATION=1 $(SHELLSPEC) spec/integration
golden-record: ## Re-record golden expectations from the legacy engine
spec/golden/record.sh $(CASES)
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)
else
include make/include.mk
endif
+93 -165
View File
@@ -1,6 +1,12 @@
# myos - Make Your Own Stack
Make Your Own Stack provides common make targets to build and run docker projects.
myos runs docker compose stacks: on a server, in a project directory, for a user.
It is a thin layer over `docker compose` that resolves which compose files to load,
under which project name, with which environment variables.
The framework itself ships no stack. Ready-to-use stacks (consul, fabio, registrator,
postgres, supabase, drone, …) live in a separate catalogue,
[myos-stacks](https://github.com/aya/myos-stacks).
## Disclaimer
@@ -8,197 +14,119 @@ This is beta software, use it at your own risks.
## Requirements
You need `docker`, `git` and `make`.
`docker` (with the `docker compose` plugin >= 2.24.4, or a `docker-compose` binary),
`git` and `make`.
## Install
* Include MYOS file `make/include.mk` adding the following lines to your project file `Makefile`.
### As a command
```sh
sudo git clone https://github.com/aya/myos /usr/local/lib/myos
sudo ln -s /usr/local/lib/myos/myos /usr/local/bin/myos
```
MYOS ?= ../myos
MYOS_REPOSITORY ?= $(patsubst %/$(THIS),%/myos,$(THIS_REPOSITORY))
THIS ?= $(lastword $(subst /, ,$(THIS_REPOSITORY)))
THIS_REPOSITORY ?= $(shell git config --get remote.origin.url 2>/dev/null)
$(MYOS):
-@git clone $(MYOS_REPOSITORY) $(MYOS)
Optionally pin per-machine settings in `/etc/conf.d/myos` (or `/etc/default/myos`),
one `VAR=value` per line, no comments:
```sh
DOMAIN=example.org
ENV=master
```
`myos` runs from the current directory: it passes it as `WORKDIR`, so stacks and
`.env` are looked up there. A `WORKDIR` set in the config file wins over the current
directory, which pins a machine to its deployment directory.
### As a make include
Add to your project `Makefile`:
```make
MYOS ?= /usr/local/lib/myos
-include $(MYOS)/make/include.mk
```
* Call the `make help` command to show available targets.
Then `make help` lists the available targets.
```
$ make help
Usage:
make [target]
### Stack catalogue
Targets:
help This help
[...]
myos looks for stacks in `./stack`, `../stack`, `~/.local/share/myos/stack`,
`/usr/local/share/myos/stack` and `/usr/share/myos/stack`:
```sh
sudo git clone https://github.com/aya/myos-stacks /usr/local/share/myos
```
## Usage
### Examples
* Configure myos for domain `domain.tld` and stack `default`
```shell
$ make bootstrap DOMAIN=domain.tld STACK=default
```sh
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/fabio # a single stack
myos up STACK=postgres:9.6 # a versioned stack
myos ps
myos logs
myos config # rendered compose file
myos down
myos shutdown # every stack: app, host and user
```
* Start myos stack `host`
### How a stack is resolved
```shell
$ make host
```
`make host` starts the stack `host` with docker host services :
- consul (service discovery) on host port 8500
- fabio (load balancer) on host ports 80 and 443
- registrator (docker/consul bridge)
* Stop myos
```shell
$ make shutdown
```
### Variables
* DEBUG
Show executed commands.
```shell
$ make up DEBUG=true
```
* DRYRUN
Do nothing, show commands instead of executing it.
```shell
$ make up DRYRUN=true
```
* VERBOSE
Show called functions.
```shell
$ make up VERBOSE=true
```
* Show variable USER
```shell
$ make print-USER
```
#### Setup
* SETUP_LETSENCRYPT
Generate ${DOMAIN} certificate files with letsencrypt.
By default, myos generates invalid ${DOMAIN} certificate files with openssl.
You can use letsencrypt instead, to generate valid wildcard certificate files.
To achieve this, you must add following DNS entries to domain ${DOMAIN} to prove you own it:
A stack is a directory of compose files. For `STACK=<name>` in environment `ENV`,
myos loads, in order, whichever of these exist:
```
_acme-challenge.${DOMAIN} IN CNAME ${DOMAIN}.acme.${DOMAIN}.
acme.${DOMAIN}. IN NS certbot.${DOMAIN}.
certbot.${DOMAIN}. IN A ${DOCKER_HOST_INET4}
<name>.yml <name>.<ENV>.yml <ENV>/<name>.yml
<name>.<suffix>.yml <name>.<suffix>.<ENV>.yml <name>.<version>.yml
```
In this config, DOCKER_HOST_INET4 should be the external IP address of the server running certbot.
Port 53 of this IP address must be reachable from internet and point to this server.
`<suffix>` comes from the `COMPOSE_FILE_*` variables that are not `false`
(`app`, `labels`, `networks`, `ssh`, `volumes` by default; add e.g.
`COMPOSE_FILE_WWW=true` to also load `<name>.www.yml`). The framework always
appends its own `share/compose/networks.yml`.
If you want a simple DNS configuration to host all your services on the same server, you can setup following DNS config:
### Project names and networks
```
@ IN A ${DOCKER_HOST_INET4}
*.${DOMAIN}. IN CNAME ${DOMAIN}.
_acme-challenge.${DOMAIN} IN CNAME ${DOMAIN}.acme.${DOMAIN}.
acme.${DOMAIN}. IN NS ${DOMAIN}.
| stack | compose project | meaning |
|---|---|---|
| `host/*` | `$(HOSTNAME)` | one instance per machine: ports 80/443, consul, certbot |
| `User/*` | user identity | one instance per user |
| anything else | `<user>-<app>-<env>` | many instances per machine |
Networks: `default` = `_<project>` (private to the project), `private` =
`<user>-<env>` and `public` = `<hostname>`, both external and created on demand.
## Variables
| variable | effect |
|---|---|
| `DEBUG=true` | show executed commands |
| `DRYRUN=true` | print commands instead of running them |
| `VERBOSE=true` | show called functions |
| `ENV=<env>` | environment: selects `.env.<env>` and the `<name>.<env>.yml` overlays |
| `STACK=<refs>` | stacks to act on |
| `SERVICE=<name>` | target one compose service (`exec`, `run`, `logs`, `scale`) |
```sh
myos print-COMPOSE_FILE # show a variable
myos print-COMPOSE_PROJECT_NAME
myos debug # show debug variables
myos doc # self documentation from the make comments
```
This will point domain ${DOMAIN} to the IP address ${DOCKER_HOST_INET4} of this server, and point all subdomains *.{DOMAIN} to the ip address pointed by ${DOMAIN}.
`SETUP_UFW=true` enables the ufw/ufw-docker integration (`myos setup-ufw`).
At this point, you should be able to generate a valid certificate for *.${DOMAIN} using certbot [dns standalone](https://github.com/siilike/certbot-dns-standalone) plugin.
This task is done automatically when creating the host stack if SETUP_LETSENCRYPT variable is not empty.
## Tests
If you already launched myos host stack before, the ${DOMAIN} certificates has been automatically generated by openssl and you should remove them before trying to generate them with letsencrypt.
```
$ make host-down
$ docker volume rm $(hostname)
```sh
make test # shellspec: unit + golden (a mocked docker, no daemon needed)
make test-golden # golden only
make golden-record # re-record the golden expectations
make lint # shellcheck
```
You can then test the letsencrypt certificate generation using DEBUG mode that force to use the letsencrypt staging server.
```
$ make host SETUP_LETSENCRYPT=true DEBUG=true
```
If letsencrypt certificate generation fails, you can retry the generation of a staging certificate.
```
$ make host-certbot-staging
```
Once the certificate generation is working, you can ask for a valid certificate.
```
$ make host-down
$ docker volume rm $(hostname)
$ make host SETUP_LETSENCRYPT=true
```
* SETUP_UFW
Control linux firewall rules with ufw.
```
$ echo SETUP_UFW=true >> .env
$ make setup-ufw
```
### Debug
* Show docker compose yaml config
```shell
$ make config
```
`make config` show docker compose yaml config for stack `STACK`
`make host-config` show docker compose yaml config for stack `host`
`make user-config` show docker compose yaml config for stack `User`
`make stack-elastic-config` show docker compose yaml config for stack `elastic`
* Show debug variables
```shell
$ make debug
```
* Generate self documentation
```shell
$ make doc
```
* Show env args
```shell
$ make print-env_args
```
* Show user mail
```shell
$ make print-MAIL
```
## License
GPL-3.0, see LICENSE.
Executable
+206
View File
@@ -0,0 +1,206 @@
#!/bin/sh
# myos - Make Your Own Stack
#
# shellcheck disable=SC2034 # most globals here are read by lib/ and lib/cmd/
# shellcheck disable=SC1091 # lib files are sourced by path at runtime
#
# Runs docker compose stacks: on a host, in a project directory, for a user.
# See README.md, or `myos help`.
set -u
MYOS_VERSION=2.0.0-dev
# --- locate the installation ---------------------------------------------
_self=$0
while [ -L "$_self" ]; do
_link=$(readlink "$_self")
case $_link in /*) _self=$_link ;; *) _self=$(dirname "$_self")/$_link ;; esac
done
MYOS_ROOT=$(cd "$(dirname "$_self")/.." && pwd -P)
export MYOS_ROOT
for _m in core str tags naming stack config compose; do
# shellcheck source=/dev/null
. "$MYOS_ROOT/lib/$_m.sh"
done
# --- command line ---------------------------------------------------------
# These are read by the lib/cmd/* files sourced further down.
# shellcheck disable=SC2034
{
MYOS_CMD=
MYOS_REFS=
MYOS_REFS_RAW=
MYOS_VARS=
MYOS_ARGS=
MYOS_HOSTS=
VERBOSE=${VERBOSE:-}
DEBUG=${DEBUG:-}
}
usage() {
cat <<'USAGE'
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...]
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
USAGE
}
while [ $# -gt 0 ]; do
case $1 in
-C) WORKDIR=$2; shift 2 ;;
-e) ENV=$2; shift 2 ;;
-H) MYOS_HOSTS=$2; shift 2 ;;
-n|--dry-run) DRYRUN=true; shift ;;
-v|--verbose) VERBOSE=true; shift ;;
-d|--debug) DEBUG=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; MYOS_ARGS="$*"; break ;;
-*)
if [ -n "$MYOS_CMD" ]; then
MYOS_ARGS="${MYOS_ARGS:+$MYOS_ARGS }$1"; shift
else
myos_error "unknown option: $1"; usage >&2; exit "$MYOS_E_USAGE"
fi ;;
*=*) eval "${1%%=*}=\${1#*=}"; export "${1%%=*}"; shift ;;
*)
if [ -z "$MYOS_CMD" ]; then MYOS_CMD=$1; else MYOS_REFS="${MYOS_REFS:+$MYOS_REFS }$1"; fi
shift ;;
esac
done
[ -n "$MYOS_CMD" ] || { usage; exit "$MYOS_E_USAGE"; }
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>,
# <command>@<env> and a bare group name.
case $MYOS_CMD in
print-*)
MYOS_VARS=${MYOS_CMD#print-}
# shellcheck disable=SC2209 # the literal string "env", not the command
MYOS_CMD=env ;;
*@*) ENV=${MYOS_CMD#*@}; MYOS_CMD=${MYOS_CMD%@*} ;;
esac
case $MYOS_CMD in
stack-*-*)
_rest=${MYOS_CMD#stack-}
MYOS_CMD=${_rest##*-}
MYOS_REFS="${_rest%-*} $MYOS_REFS"
MYOS_REFS=${MYOS_REFS% } ;;
esac
# --- configuration --------------------------------------------------------
WORKDIR=${WORKDIR:-$PWD}
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
myos_dotenv_load "${HOME:-}/.config/myos/config"
ENV=${ENV:-local}
myos_dotenv_load "$WORKDIR/.env.$ENV"
myos_dotenv_load "$WORKDIR/.env"
# Overlay switches. Their names drive which <stack>.<suffix>.yml files load,
# so these defaults decide that e.g. supabase.labels.yml is picked up.
COMPOSE_FILE_APP=${COMPOSE_FILE_APP:-true}
COMPOSE_FILE_LABELS=${COMPOSE_FILE_LABELS:-true}
COMPOSE_FILE_NETWORKS=${COMPOSE_FILE_NETWORKS:-true}
COMPOSE_FILE_SSH=${COMPOSE_FILE_SSH:-true}
COMPOSE_FILE_VOLUMES=${COMPOSE_FILE_VOLUMES:-true}
COMPOSE_FILE_DEBUG=${COMPOSE_FILE_DEBUG:-${DEBUG:+true}}
USER=${USER:-$(id -nu 2>/dev/null)}
HOSTNAME=${HOSTNAME:-$(hostname 2>/dev/null | sed 's/\..*//')}
HOSTNAME=$(myos_lower "$HOSTNAME")
DOMAIN=${DOMAIN:-localhost}
DOMAINNAME=${DOMAINNAME:-${DOMAIN%% *}}
MAIL=${MAIL:-$(git config user.email 2>/dev/null || printf '%s@%s' "$USER" "$DOMAINNAME")}
DRYRUN=${DRYRUN:-false}
myos_colors
# --- stack references -----------------------------------------------------
# No reference given: the configured STACK, else the current directory when it
# holds a compose file. An explicit STACK always wins, so a project that pins
# its stacks in .env keeps working from inside its own directory.
if [ -z "$MYOS_REFS" ]; then
if [ -n "${STACK:-}" ]; then
MYOS_REFS=$STACK
elif [ -f "$WORKDIR/docker-compose.yml" ] || [ -f "$WORKDIR/compose.yml" ] ||
[ -f "$WORKDIR/docker/docker-compose.yml" ]; then
MYOS_REFS=./
fi
fi
if [ -z "$MYOS_REFS" ]; then
case $MYOS_CMD in
env|ls|doctor|version|help) ;;
*) myos_die "$MYOS_E_USAGE" "no stack given, and no compose file in $WORKDIR" ;;
esac
fi
# shellcheck disable=SC2086 # a list of references
MYOS_STACKS=$(myos_group_expand $MYOS_REFS)
# 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.
myos_framework_compose_files() {
myos_compose_files "$MYOS_ROOT/share/compose" "networks volumes" "$(myos_compose_suffixes)" "$ENV"
}
# myos_stack_compose_files REF the ordered compose files of one reference
myos_stack_compose_files() {
_dir=$(myos_stack_resolve "$1") || return $?
_name=$(myos_stack_name "$1")
_suffixes="$(myos_compose_suffixes) $(myos_stack_version "$1")"
case $1 in
.|./*|/*|../*)
myos_compose_files "$_dir" "docker-compose compose" "$_suffixes" "$ENV"
myos_compose_files "$_dir/docker" "docker-compose compose" "$_suffixes" "$ENV" ;;
*)
myos_compose_files "$_dir" "docker-compose $_name" "$_suffixes" "$ENV" ;;
esac
}
# --- dispatch -------------------------------------------------------------
case $MYOS_CMD in
version) printf 'myos %s\n' "$MYOS_VERSION"; exit 0 ;;
help) usage; exit 0 ;;
esac
# shellcheck source=/dev/null
if [ -f "$MYOS_ROOT/lib/cmd/$MYOS_CMD.sh" ]; then
. "$MYOS_ROOT/lib/cmd/$MYOS_CMD.sh"
"myos_cmd_$MYOS_CMD"
exit $?
fi
# compose passthrough commands
case $MYOS_CMD in
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" ;;
esac
. "$MYOS_ROOT/lib/cmd/_compose.sh"
myos_cmd_compose "$MYOS_CMD"
-20
View File
@@ -1,20 +0,0 @@
FROM alpine:latest as dist
LABEL maintainer aynic.os <support+docker@asycn.io>
ARG DOCKER_BUILD_DIR
ARG COMPOSE_REMOTE=https://github.com/docker/compose
ARG COMPOSE_VERSION=2.5.0
ARG DOCKER_MACHINE=x86_64
ARG DOCKER_SYSTEM=Linux
RUN apk update \
&& apk add --no-cache ca-certificates \
&& OS="$(echo ${DOCKER_SYSTEM} |awk '{print tolower($0)}')" \
&& ARCH="$(echo ${DOCKER_MACHINE} |awk '{print /armv7l/ ? "armv7" : $0}')" \
&& wget -qO /usr/bin/docker-compose ${COMPOSE_REMOTE}/releases/download/v${COMPOSE_VERSION}/docker-compose-${OS}-${ARCH} \
&& chmod +x /usr/bin/docker-compose
ENTRYPOINT ["/usr/bin/docker-compose"]
FROM dist as master
ARG DOCKER_BUILD_DIR
+75
View File
@@ -0,0 +1,75 @@
#shellcheck shell=sh
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
# The commands that map straight onto docker compose.
#
# Stacks are grouped by compose project: every host stack shares the project of
# the machine, so `myos up host` is a single compose call with every file, the
# way the stack was meant to be described.
# myos_cmd_compose COMMAND
myos_cmd_compose() {
_cmd=$1
_projects=
_rc=0
for _ref in $MYOS_STACKS; do
_files=$(myos_stack_compose_files "$_ref") || { _rc=$MYOS_E_NOSTACK; continue; }
[ -n "$_files" ] || { myos_warning "no compose file for stack $_ref"; continue; }
_scope=$(myos_scope "$_ref")
_app=$(myos_stack_name "$_ref")
case $_ref in .|./*|/*|../*) _app=$(basename "$(myos_stack_resolve "$_ref")") ;; esac
_project=$(myos_project_name "$_scope" "$USER" "$ENV" "$_app")
# accumulate the files of every stack sharing a project, keeping the order
_projects=$(printf '%s\n%s\t%s' "$_projects" "$_project" "$(printf '%s' "$_files" | tr '\n' ' ')")
done
[ "$_rc" = 0 ] || return "$_rc"
for _project in $(printf '%s' "$_projects" | sed '/^$/d' | cut -f1 | awk '!seen[$0]++'); do
_files=$(printf '%s' "$_projects" | sed '/^$/d' | awk -F'\t' -v p="$_project" '$1==p {print $2}' | tr ' ' '\n' | sed '/^$/d' | awk '!seen[$0]++')
# the framework overlays always come last, as the make engine did
_fw=$(myos_framework_compose_files)
[ -n "$_fw" ] && _files="$_files
$_fw"
COMPOSE_PROJECT_NAME=$_project
COMPOSE_SERVICE_NAME=$(myos_service_name "$_project")
DOCKER_NETWORK_DEFAULT=${DOCKER_NETWORK_DEFAULT:-$(myos_network_default "$_project")}
DOCKER_NETWORK_PRIVATE=$(myos_network_private "$USER" "$ENV")
# shellcheck disable=SC3028 # HOSTNAME is set by bin/myos, not by the shell
DOCKER_NETWORK_PUBLIC=$(myos_network_public "$HOSTNAME")
export COMPOSE_PROJECT_NAME COMPOSE_SERVICE_NAME
export DOCKER_NETWORK_DEFAULT DOCKER_NETWORK_PRIVATE DOCKER_NETWORK_PUBLIC
case $_cmd in
up) myos_network_ensure "$DOCKER_NETWORK_PRIVATE" "$DOCKER_NETWORK_PUBLIC" ;;
esac
# shellcheck disable=SC2086,SC2046 # options and MYOS_ARGS are word lists
myos_compose "$_project" "$_files" -- "$_cmd" $(myos_compose_options "$_cmd") ${MYOS_ARGS:-} || _rc=$?
done
return "$_rc"
}
# myos_compose_options COMMAND the default options of each compose command
myos_compose_options() {
case $1 in
up) printf '%s' "${DOCKER_COMPOSE_UP_OPTIONS:--d}" ;;
logs) printf '%s' "${DOCKER_COMPOSE_LOGS_OPTIONS:---follow --tail=100}" ;;
down) printf '%s' "${DOCKER_COMPOSE_DOWN_OPTIONS:-}" ;;
*) printf '' ;;
esac
}
# myos_network_ensure NAME... create the external networks if they are missing
myos_network_ensure() {
for _n in "$@"; do
[ -n "$_n" ] || continue
if [ "${DRYRUN:-false}" = true ]; then
printf 'docker network create %s\n' "$_n"
else
docker network inspect "$_n" >/dev/null 2>&1 || myos_run docker network create "$_n" >/dev/null
fi
done
return 0
}
+39
View File
@@ -0,0 +1,39 @@
#shellcheck shell=sh
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
# myos doctor check that this installation can actually run a stack
myos_cmd_doctor() {
_rc=0
_ok() { printf ' %-28s %s\n' "$1" "$2"; }
_bad() { printf ' %-28s %s%s%s\n' "$1" "$MYOS_C_ERROR" "$2" "$MYOS_C_RESET"; _rc=$MYOS_E_NOREQ; }
printf 'myos %s at %s\n' "$MYOS_VERSION" "$MYOS_ROOT"
printf 'requirements:\n'
if myos_have docker; then _ok docker "$(docker version --format '{{.Client.Version}}' 2>/dev/null || echo present)"
else _bad docker "not found"; fi
if _c=$(myos_compose_bin 2>/dev/null); then _ok "compose" "$_c"; else _bad compose "docker compose >= $MYOS_COMPOSE_MIN_VERSION not found"; fi
if [ "${DRYRUN:-false}" != true ]; then
if docker info >/dev/null 2>&1; then _ok "docker daemon" "reachable${DOCKER_HOST:+ via $DOCKER_HOST}"
else _bad "docker daemon" "unreachable${DOCKER_HOST:+ ($DOCKER_HOST)}"; fi
fi
printf 'configuration:\n'
for _f in $(myos_conf_files); do _ok "$_f" "read"; done
[ -f "${HOME:-}/.config/myos/config" ] && _ok "${HOME:-}/.config/myos/config" "read"
[ -f "$WORKDIR/.env" ] && _ok "$WORKDIR/.env" "read"
_ok ENV "$ENV"
_ok USER "$USER"
_ok HOSTNAME "$HOSTNAME"
_ok DOMAIN "$DOMAIN"
_ok "project format" "${MYOS_PROJECT_FORMAT:-user-env-app}"
printf 'stacks:\n'
_p=$(myos_path)
if [ -n "$_p" ]; then printf '%s\n' "$_p" | tr ':' '\n' | sed 's/^/ /'
else _bad "stack path" "empty"; fi
# a .env written for the make engine can hold values the shell reads differently
if [ -f "$WORKDIR/.env" ] && grep -qE '\$\(|\$\{[a-z]' "$WORKDIR/.env"; then
printf ' %-28s %s%s%s\n' "$WORKDIR/.env" "$MYOS_C_WARN" "contains make expansions" "$MYOS_C_RESET"
fi
return "$_rc"
}
+67
View File
@@ -0,0 +1,67 @@
#shellcheck shell=sh
# 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_cmd_env() {
_vars=${MYOS_VARS:-}
[ -n "$_vars" ] || _vars=$MYOS_ARGS
[ -n "$_vars" ] || _vars=$MYOS_REFS_RAW
if [ -z "$_vars" ]; then
_vars="ENV USER HOSTNAME DOMAIN WORKDIR MYOS_PATH STACK COMPOSE_PROJECT_NAME COMPOSE_FILE"
fi
for _v in $_vars; do
case $_v in
MYOS_PATH) printf '%s %s\n' "$_v" "$(myos_path)" ;;
COMPOSE_FILE) printf '%s %s\n' "$_v" "$(myos_all_compose_files | tr '\n' ' ' | sed 's/ $//')" ;;
COMPOSE_PROJECT_NAME) printf '%s %s\n' "$_v" "$(myos_first_project)" ;;
STACK) printf '%s %s\n' "$_v" "$(printf '%s' "$MYOS_STACKS" | tr '\n' ' ' | sed 's/ $//')" ;;
DOCKER_NETWORK_DEFAULT) printf '%s %s\n' "$_v" "$(myos_network_default "$(myos_first_project)")" ;;
DOCKER_NETWORK_PRIVATE) printf '%s %s\n' "$_v" "$(myos_network_private "$USER" "$ENV")" ;;
DOCKER_NETWORK_PUBLIC) printf '%s %s\n' "$_v" "$(myos_network_public "${HOSTNAME:-}")" ;;
COMPOSE_SERVICE_NAME) printf '%s %s\n' "$_v" "$(myos_service_name "$(myos_first_project)")" ;;
COMPOSE_FILE_SUFFIX) printf '%s %s\n' "$_v" "$(myos_compose_suffixes)" ;;
APP|APP_NAME) printf '%s %s\n' "$_v" "$(myos_first_app)" ;;
SCOPE) printf '%s %s\n' "$_v" "$(myos_first_scope)" ;;
# HOST_STACK/USER_STACK are the names the make engine used for the scope
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)
if [ "$(myos_first_scope)" = user ]; then printf '%s %s\n' "$_v" "$USER"
else printf '%s %s\n' "$_v" "$(myos_network_private "$USER" "$ENV")"; fi ;;
*) printf '%s %s\n' "$_v" "$(myos_var "$_v")" ;;
esac
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
}
+36
View File
@@ -0,0 +1,36 @@
#shellcheck shell=sh
# myos ls [--groups] list the stacks myos can see, and where they come from
myos_cmd_ls() {
case ${MYOS_REFS_RAW:-}${MYOS_ARGS:-} in
*--groups*) myos_ls_groups; return 0 ;;
esac
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
printf '%s%s%s\n' "$MYOS_C_INFO" "$_d" "$MYOS_C_RESET"
for _s in "$_d"/*; do
[ -d "$_s" ] || continue
_n=$(basename "$_s")
_f=$(find "$_s" -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) | wc -l | tr -d ' ')
[ "$_f" = 0 ] && continue
printf ' %-24s %s compose file(s)\n' "$_n" "$_f"
done
IFS=:
done
IFS=$_IFS
}
myos_ls_groups() {
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
for _f in "$_d"/*.mk "$_d"/*.env "$_d"/*/*.mk; do
[ -f "$_f" ] || continue
_n=$(basename "$_f"); _n=${_n%.mk}; _n=${_n%.env}
_v=$(myos_group_value "$_n")
[ -n "$_v" ] && printf '%-16s %s\n' "$_n" "$_v"
done
IFS=:
done
IFS=$_IFS
}
+60
View File
@@ -0,0 +1,60 @@
#shellcheck shell=sh
# compose: find a usable docker compose, and call it once per project.
#
# The legacy engine ran one `docker compose up` per sub-stack, each time with
# the full file list, so N sub-stacks meant N identical calls. The CLI calls
# compose once per project (documented in spec/golden/DELTAS.md).
MYOS_COMPOSE_MIN_VERSION=${COMPOSE_VERSION:-2.24.4}
# myos_compose_bin print the compose command to use, fail with MYOS_E_NOREQ
myos_compose_bin() {
[ -n "${MYOS_COMPOSE_BIN:-}" ] && { printf '%s' "$MYOS_COMPOSE_BIN"; return 0; }
if myos_have docker; then
_v=$(docker compose version --short 2>/dev/null)
if myos_verle "$MYOS_COMPOSE_MIN_VERSION" "$_v"; then printf 'docker compose'; return 0; fi
fi
if myos_have docker-compose; then
_v=$(docker-compose version --short 2>/dev/null)
if myos_verle "$MYOS_COMPOSE_MIN_VERSION" "$_v"; then printf 'docker-compose'; return 0; fi
fi
myos_error "docker compose >= $MYOS_COMPOSE_MIN_VERSION not found (install the docker compose plugin or docker-compose)"
return "$MYOS_E_NOREQ"
}
# myos_compose PROJECT FILES -- ARGS...
# FILES is a newline separated list; the project directory is that of the first
# file, so relative build contexts and env_file entries keep working.
myos_compose() {
_project=$1; _files=$2; shift 2
[ "${1:-}" = "--" ] && shift
[ -n "$_files" ] || { myos_error "no compose file for project $_project"; return "$MYOS_E_NOSTACK"; }
_bin=$(myos_compose_bin) || return $?
_fargs=""
_first=""
for _f in $_files; do
[ -n "$_first" ] || _first=$_f
_fargs="$_fargs -f $_f"
done
_dir=$(dirname "$_first")
# variables the compose files reference, plus the network names they default
# on (networks.yml is appended after the scan, so its variables are added here)
# shellcheck disable=SC2086 # both are deliberate word lists
_vars=$(myos_env_vars $_files)
# shellcheck disable=SC2086
_envargs=$(myos_env_export $_vars DOCKER_NETWORK_DEFAULT DOCKER_NETWORK_PRIVATE DOCKER_NETWORK_PUBLIC COMPOSE_SERVICE_NAME)
if [ "${DRYRUN:-false}" = true ]; then
printf '%s%s -p %s --project-directory %s %s\n' "$_bin" "$_fargs" "$_project" "$_dir" "$*"
else
_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
}
+82
View File
@@ -0,0 +1,82 @@
#shellcheck shell=sh
# config: where settings come from, and in which order.
#
# Layers, last one wins:
# defaults < /etc/conf.d/myos, /etc/default/myos < ~/.config/myos/config
# < <workdir>/.env < <workdir>/.env.<env> < environment < CLI VAR=val
# MYOS_CONF_PRIORITY=system restores the old make behaviour where the system
# file won over the project .env.
#
# Files are dotenv: KEY=value, one per line, # comments, optional quotes.
# They are parsed, never sourced: a value never runs as code.
# myos_dotenv_parse FILE print normalized KEY=value lines
myos_dotenv_parse() {
[ -f "$1" ] || return 0
sed -e 's/\r$//' -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' "$1" |
while IFS= read -r _line; do
case $_line in *=*) ;; *) continue ;; esac
_k=${_line%%=*}
_v=${_line#*=}
_k=$(printf '%s' "$_k" | tr -d '[:space:]')
# whitespace around the = is not part of the value (make: s/[[:space:]]*=[[:space:]]*/=/)
_v=${_v#"${_v%%[![:space:]]*}"}
case $_k in ''|*[!A-Za-z0-9_]*) continue ;; esac
# strip one layer of matching quotes
case $_v in
\"*\") _v=${_v#\"}; _v=${_v%\"} ;;
\'*\') _v=${_v#\'}; _v=${_v%\'} ;;
esac
printf '%s=%s\n' "$_k" "$_v"
done
}
# myos_dotenv_load FILE set the variables of FILE that are not already set
# (an already exported variable wins, as `?=` does in make)
myos_dotenv_load() {
[ -f "$1" ] || return 0
while IFS= read -r _kv; do
# an empty file still yields one empty line through the here-document
[ -n "$_kv" ] || continue
_k=${_kv%%=*}
[ -n "$_k" ] || continue
[ -n "$(myos_var "$_k")" ] && continue
eval "$_k=\${_kv#*=}"
done <<EOF
$(myos_dotenv_parse "$1")
EOF
}
# myos_dotenv_has FILE KEY
myos_dotenv_has() { myos_dotenv_parse "$1" | grep -q "^$2="; }
# myos_conf_files the system config files, most significant first
myos_conf_files() {
[ -n "${MYOS_CONF:-}" ] && { printf '%s\n' "$MYOS_CONF"; return 0; }
for _f in /etc/conf.d/myos /etc/default/myos; do
[ -r "$_f" ] && printf '%s\n' "$_f"
done
return 0
}
# myos_env_vars FILE... the ${VAR} names referenced by the compose files
# (make: env-vars). $$VAR is a compose escape, not a shell variable.
myos_env_vars() {
[ $# -gt 0 ] || return 0
sed 's/\$\$//g' "$@" 2>/dev/null |
grep -oE '\$\{?[A-Z0-9_]+' |
tr -d '{}$' |
sort -u |
tr '\n' ' ' |
sed 's/ $//'
}
# myos_env_export VAR... print VAR='value' for each variable that has a value,
# ready to be passed to env(1)
myos_env_export() {
for _v in "$@"; do
_val=$(myos_var "$_v")
[ -n "$_val" ] && printf "%s=%s\n" "$_v" "$_val"
done
return 0
}
+47
View File
@@ -0,0 +1,47 @@
#shellcheck shell=sh
# core: logging, error handling and command execution.
#
# Every myos command goes through myos_run, which honours DRYRUN by printing
# the command instead of running it. Messages go to stderr so that stdout stays
# usable for data (myos env, myos config, myos ls).
# Exit codes and colors are consumed by the other lib/ files and by bin/myos.
# shellcheck disable=SC2034
MYOS_E_OK=0 # success
MYOS_E_FAIL=1 # command failed
MYOS_E_USAGE=2 # bad invocation
MYOS_E_NOSTACK=3 # stack not found
MYOS_E_NOREQ=4 # missing requirement
myos_colors() {
if [ -t 2 ] && [ "${TERM:-dumb}" != dumb ] && [ -z "${NO_COLOR:-}" ]; then
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_VALUE=$(printf '\033[36m'); MYOS_C_RESET=$(printf '\033[0m')
else
MYOS_C_ERROR=; MYOS_C_WARN=; MYOS_C_INFO=; MYOS_C_DEBUG=; MYOS_C_VALUE=; MYOS_C_RESET=
fi
}
myos_error() { printf '%sERROR:%s %s\n' "$MYOS_C_ERROR" "$MYOS_C_RESET" "$*" >&2; }
myos_warning() { printf '%sWARNING:%s %s\n' "$MYOS_C_WARN" "$MYOS_C_RESET" "$*" >&2; }
myos_info() { [ -n "${VERBOSE:-}" ] && printf '%s%s%s\n' "$MYOS_C_INFO" "$*" "$MYOS_C_RESET" >&2; return 0; }
myos_debug() { [ -n "${DEBUG:-}" ] && printf '%s%s%s\n' "$MYOS_C_DEBUG" "$*" "$MYOS_C_RESET" >&2; return 0; }
# myos_die CODE MESSAGE...
myos_die() { _code=$1; shift; myos_error "$*"; exit "$_code"; }
# myos_run CMD... run a command, or print it when DRYRUN is true
myos_run() {
if [ "${DRYRUN:-false}" = true ]; then
printf '%s\n' "$*"
return 0
fi
myos_debug "+ $*"
"$@"
}
# myos_have CMD is a command available?
myos_have() { command -v "$1" >/dev/null 2>&1; }
myos_colors
+81
View File
@@ -0,0 +1,81 @@
#shellcheck shell=sh
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
# naming: compose project name, service name, networks, user identity.
#
# Ported from make/apps/def.docker.mk (COMPOSE_PROJECT_NAME, COMPOSE_SERVICE_NAME),
# make/def.docker.mk (HOST_*/USER_*, DOCKER_NETWORK_*) and make/def.mk (RESU).
# myos_scope REF -> host | user | cluster | app
# The first segment of a stack reference decides how the stack is named:
# host stacks are singletons of the machine (they bind privileged ports),
# user stacks are singletons of the user, cluster stacks are swarm namespaces.
myos_scope() {
[ -n "${MYOS_SCOPE:-}" ] && { printf '%s' "$MYOS_SCOPE"; return 0; }
case ${1%%/*} in
host) printf 'host' ;;
User|user) printf 'user' ;;
cluster) printf 'cluster' ;;
*) printf 'app' ;;
esac
}
# myos_resu MAIL -> user.domain identity of a mail address (make: RESU)
# Also sets MYOS_RESU_NIAMOD (reversed domain + reversed user) and
# MYOS_RESU_PATH (that identity as a path), used by the User stacks.
myos_resu() {
_mail=$(myos_lower "${1:-}" | tr '+_' '..')
case $_mail in
*@*) ;;
*) MYOS_RESU_NIAMOD=; MYOS_RESU_PATH=; printf '%s' "${USER:-}"; return 0 ;;
esac
_user=${_mail%@*}
_domain=${_mail##*@}
[ -n "$_domain" ] || { MYOS_RESU_NIAMOD=; MYOS_RESU_PATH=; printf '%s' "${USER:-}"; return 0; }
_niamod=$(myos_reverse "$(printf '%s' "$_domain" | tr '.' ' ')" | tr ' ' '.')
_resu=$(myos_reverse "$(printf '%s' "$_user" | tr '.' ' ')" | tr ' ' '.')
MYOS_RESU_NIAMOD="$_niamod.$_resu"
# consumed by the User stacks, not by this file
# shellcheck disable=SC2034
MYOS_RESU_PATH=$(printf '%s' "$MYOS_RESU_NIAMOD" | tr '.' '/')
printf '%s.%s' "$_user" "$_domain"
}
# myos_project_name SCOPE USER ENV APP [PATH]
# host -> HOST_COMPOSE_PROJECT_NAME, defaults to the hostname
# user -> USER_COMPOSE_PROJECT_NAME, defaults to the RESU identity
# cluster -> the stack name: one namespace per swarm, not per user
# app -> MYOS_PROJECT_FORMAT: user-env-app (default) or user-app-env (legacy)
myos_project_name() {
_scope=$1; _user=$2; _env=$3; _app=$4; _path=${5:-}
[ -n "${DOCKER_COMPOSE_PROJECT_NAME:-}" ] && { printf '%s' "$DOCKER_COMPOSE_PROJECT_NAME"; return 0; }
case $_scope in
host)
printf '%s' "${HOST_COMPOSE_PROJECT_NAME:-${HOSTNAME:-localhost}}"; return 0 ;;
user)
if [ -n "${USER_COMPOSE_PROJECT_NAME:-}" ]; then printf '%s' "$USER_COMPOSE_PROJECT_NAME"
else printf '%s' "$(myos_resu "${MAIL:-}" | tr '.' '-')"; fi
return 0 ;;
cluster)
printf '%s' "${MYOS_CLUSTER_PROJECT:-$(myos_name "$_app")}"; return 0 ;;
esac
_n=$(myos_name "$_app")
case ${MYOS_PROJECT_FORMAT:-user-env-app} in
user-app-env) _out="$_user-$_n-$_env" ;;
user-env-app) _out="$_user-$_env-$_n" ;;
*) myos_die "$MYOS_E_USAGE" "unknown MYOS_PROJECT_FORMAT: ${MYOS_PROJECT_FORMAT}" ;;
esac
# the path fragment loses its slashes too (make: $(subst /,,$(subst -,,$(APP_PATH))))
[ -n "$_path" ] && _out="$_out-$(myos_name "$_path" | tr -d /)"
myos_lower "$_out" | tr -d '.'
}
# myos_service_name PROJECT prefix of the SERVICE_<port>_NAME labels
myos_service_name() { printf '%s' "$1" | tr '_' '-'; }
# myos_network_default PROJECT
# The leading underscore keeps this network first in alphabetical order, so it
# is the first interface attached and service names never resolve across stacks.
# https://github.com/moby/libnetwork/issues/2093
myos_network_default() { printf '_%s' "$1"; }
myos_network_private() { printf '%s' "${DOCKER_NETWORK_PRIVATE:-${1}-${2}}"; }
myos_network_public() { printf '%s' "${DOCKER_NETWORK_PUBLIC:-${1}}"; }
+151
View File
@@ -0,0 +1,151 @@
#shellcheck shell=sh
# stack: where stacks live, how a reference resolves to compose files.
#
# A stack is a directory holding compose files. A reference is
# [<group>/]<name>[:<version>] resolved along MYOS_PATH
# ./ or /abs/path or rel/path the directory itself
# Ported from make/def.docker.mk (STACK_DIR/SHARE_DIR) and
# make/apps/def.docker.mk (compose-file, docker-stack, docker-stack-update).
# myos_path the stack search path, colon separated, existing directories only.
# Project first, then the shared catalogues: a project always wins over the
# catalogue installed system wide.
myos_path() {
[ -n "${MYOS_PATH:-}" ] && { printf '%s' "$MYOS_PATH"; return 0; }
_wd=${WORKDIR:-$PWD}
_name=${STACK_DIR_NAME:-stack}
_out=
for _d in "$_wd" "$_wd/.." "${HOME:-/nonexistent}/.local/share" /usr/local/share /usr/share; do
for _c in "$_d/$_name" "$_d/myos/$_name"; do
[ -d "$_c" ] || continue
_c=$(cd "$_c" && pwd -P)
case ":$_out:" in *":$_c:"*) continue ;; esac
_out="${_out:+$_out:}$_c"
done
done
printf '%s' "$_out"
}
# myos_stack_name REF the stack name: "host/fabio:1.6" -> "fabio"
# myos_stack_version REF the version, "latest" when the reference has none
# Both are pure, so a caller can use them inside a command substitution.
myos_stack_name() {
_r=${1%/}
case $_r in *:*) _r=${_r%:*} ;; esac
basename "$_r" .yml
}
myos_stack_version() {
_r=${1%/}
case $_r in *:*) printf '%s' "${_r##*:}" ;; *) printf 'latest' ;; esac
}
# myos_stack_resolve REF print the directory holding the stack,
# or fail with MYOS_E_NOSTACK
myos_stack_resolve() {
_ref=${1%/}
case $_ref in *:*) _ref=${_ref%:*} ;; esac
_name=$(myos_stack_name "$1")
# a path reference resolves to itself
case $_ref in
.|./*|/*|../*)
if [ -d "$_ref" ]; then printf '%s' "$(cd "$_ref" && pwd -P)"; return 0; fi
myos_error "no such directory: $_ref"; return "$MYOS_E_NOSTACK" ;;
esac
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
if [ -d "$_d/$_ref" ]; then printf '%s' "$_d/$_ref"; return 0; fi
if [ -f "$_d/$_ref.yml" ] || [ -f "$_d/$_ref.yaml" ]; then
printf '%s' "$(dirname "$_d/$_ref")"; return 0
fi
if [ -d "$_d/$_name" ]; then printf '%s' "$_d/$_name"; return 0; fi
IFS=:
done
IFS=$_IFS
myos_error "stack not found: $1 (searched $(myos_path))"
return "$MYOS_E_NOSTACK"
}
# myos_compose_suffixes the overlay suffixes, from the COMPOSE_FILE_* variables
# that are not false (make: COMPOSE_FILE_SUFFIX). A value other than true also
# yields "<suffix>.<value>", which is how COMPOSE_FILE_WWW=nginx works.
myos_compose_suffixes() {
_out=
for _v in $(set | sed -n 's/^\(COMPOSE_FILE_[A-Z0-9_]*\)=.*/\1/p' | sort -u); do
case $_v in COMPOSE_FILE_SUFFIX) continue ;; esac
_val=$(myos_var "$_v")
case $_val in false|False|FALSE|'') continue ;; esac
_s=$(myos_lower "${_v#COMPOSE_FILE_}")
_out="${_out:+$_out }$_s"
case $_val in true|True|TRUE) ;; *) for _x in $_val; do _out="$_out $_s.$_x"; done ;; esac
done
printf '%s' "$_out"
}
# myos_compose_files DIR NAMES SUFFIXES [ENV]
# Print the compose files that exist, in the order the framework loads them.
myos_compose_files() {
_dir=$1; _names=$2; _suffixes=${3:-}; _env=${4:-${ENV:-local}}
for _e in yml yaml; do
for _n in $_names; do
for _f in \
"$_dir/$_n.$_e" "$_dir/$_n.$_env.$_e" \
"$_dir/$_env/$_n.$_e" "$_dir/$_env/$_n.$_env.$_e"; do
[ -f "$_f" ] && printf '%s\n' "$_f"
done
for _s in $_suffixes; do
for _f in "$_dir/$_n.$_s.$_e" "$_dir/$_n.$_s.$_env.$_e"; do
[ -f "$_f" ] && printf '%s\n' "$_f"
done
done
done
done
return 0
}
# myos_group_expand REF... expand group references recursively.
# A group is a variable whose name is the reference: `host=host/consul host/fabio`
# in the environment, in a .env, in <path>/<group>.env or in a legacy <group>.mk.
myos_group_expand() {
_depth=${MYOS_GROUP_DEPTH:-0}
[ "$_depth" -gt 16 ] && myos_die "$MYOS_E_USAGE" "stack group nested too deep: $*"
for _ref in "$@"; do
_val=$(myos_group_value "$_ref")
if [ -n "$_val" ]; then
# shellcheck disable=SC2086 # the group value is a list of references
MYOS_GROUP_DEPTH=$((_depth + 1)) myos_group_expand $_val
else
printf '%s\n' "$_ref"
fi
done
}
# myos_group_value REF the list a group expands to, empty when not a group.
# Groups are lowercase by convention (host, testing, coroot, default): without
# that rule any environment variable sharing a stack name would be expanded,
# which is how the make engine behaved. The character class is spelled out
# because a-z also matches uppercase under a dictionary collation (fr_FR).
myos_group_value() {
case $1 in .|/*|*/*|*:*) return 0 ;; esac
case $1 in *[![:lower:][:digit:]_-]*) return 0 ;; esac
_v=$(myos_var "$1")
[ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
_IFS=$IFS; IFS=:
for _d in $(myos_path); do
IFS=$_IFS
[ -f "$_d/$1.env" ] && { _v=$(sed -n "s/^$1=//p" "$_d/$1.env" | tail -1 | tr -d '"'); }
[ -z "$_v" ] && [ -f "$_d/$1.mk" ] && { _v=$(myos_mk_group "$_d/$1.mk" "$1"); }
[ -z "$_v" ] && [ -f "$_d/$1/$1.mk" ] && { _v=$(myos_mk_group "$_d/$1/$1.mk" "$1"); }
[ -n "$_v" ] && { printf '%s' "$_v"; return 0; }
IFS=:
done
IFS=$_IFS
return 0
}
# myos_mk_group FILE NAME read `name ?= a b c` out of a legacy .mk snippet
myos_mk_group() {
sed -n "s/^$2[[:space:]]*[?:]\{0,1\}=[[:space:]]*//p" "$1" 2>/dev/null | tail -1
}
+33
View File
@@ -0,0 +1,33 @@
#shellcheck shell=sh
# str: string helpers ported from make/utils.mk and make/def.mk.
# myos_lower STRING / myos_upper STRING
myos_lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
myos_upper() { printf '%s' "$1" | tr '[:lower:]-.' '[:upper:]__'; }
# myos_name STRING compose-project-safe name: lowercase, no . - _
# (make: $(subst _,,$(subst -,,$(subst .,,$(call LOWERCASE,$(1))))))
myos_name() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '._-'; }
# myos_slugify STRING keep [a-z0-9_], everything else becomes _
myos_slugify() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_]/_/g'; }
# myos_reverse WORDS... reverse the order of space separated words
myos_reverse() {
_out=
for _w in $1; do _out="$_w${_out:+ }$_out"; done
printf '%s' "$_out"
}
# myos_verle A B true when version A <= B (make: verle)
myos_verle() {
[ -n "$1" ] || return 1
[ -n "$2" ] || return 1
[ "$1" = "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" ]
}
# myos_verlt A B true when version A < B
myos_verlt() {
[ "$1" = "$2" ] && return 1
myos_verle "$1" "$2"
}
+73
View File
@@ -0,0 +1,73 @@
#shellcheck shell=sh
# tags: fabio route tags derived from stack variables.
#
# Ported from make/apps/def.mk (uri, url, urlprefix, urlprefixs, tagprefix,
# envprefix, servicenvs). Registrator publishes the SERVICE_<port>_TAGS label
# to consul, fabio routes on the urlprefix- tags it finds there.
# myos_var NAME value of the variable named NAME, empty when unset
myos_var() {
[ -n "${1:-}" ] || return 0
eval "printf '%s' \"\${$1:-}\""
}
# myos_uri SERVICE PORT [BASE_URI]
# <service>.<base uri>, unless <SERVICE>_SERVICE[_<port>]_NAME overrides the prefix
myos_uri() {
_svc=$1; _port=${2:-}; _base=${3:-${APP_URI:-}}
_u=$(myos_upper "$_svc")
_name=$(myos_var "${_u}_SERVICE_${_port}_NAME")
[ -n "$_name" ] || _name=$(myos_var "${_u}_SERVICE_NAME")
[ -n "$_name" ] || _name=$_svc
_out=
for _b in $_base; do _out="${_out:+$_out }${_name}.${_b}"; done
printf '%s' "$_out"
}
# myos_url SERVICE PORT [BASE_URI]
myos_url() {
_out=
for _u in $(myos_uri "$@"); do _out="${_out:+$_out }${APP_SCHEME:-http}://$_u"; done
printf '%s' "$_out"
}
# myos_urlprefix [PATH] [OPTS] [URIS]
# one comma separated "urlprefix-<uri><path>* [opts]" per uri
myos_urlprefix() {
_path=${1:-}; _opts=${2:-}; _uris=${3:-${APP_URI:-}}
_out=
for _u in $_uris; do
_tag="urlprefix-${_u}${_path}${MYOS_URL_SUFFIX:-*}${_opts:+ $_opts}"
_out="${_out:+$_out,}$_tag"
done
printf '%s' "$_out"
}
# myos_envprefix STACK PORT KEYS...
# "key=value" for each <STACK>_SERVICE_<port>_<KEY> that is set
myos_envprefix() {
_stack=$1; _port=$2; shift 2
_out=
for _k in "$@"; do
_v=$(myos_var "$(myos_upper "${_stack}_SERVICE_${_port}_${_k}")")
[ -n "$_v" ] && _out="${_out:+$_out }${_k}=${_v}"
done
printf '%s' "$_out"
}
# myos_tagprefix STACK PORT [URI_KEYS...]
# the fabio tag of a service, assembled from its PATH, OPTS and URIS variables
myos_tagprefix() {
_stack=$1; _port=$2; shift 2
_u=$(myos_upper "$_stack")
_path=$(myos_var "${_u}_SERVICE_${_port}_PATH"); [ -n "$_path" ] || _path=$(myos_var "${_u}_SERVICE_PATH")
_opts=$(myos_var "${_u}_SERVICE_${_port}_OPTS"); [ -n "$_opts" ] || _opts=$(myos_var "${_u}_SERVICE_OPTS")
[ -n "$_opts" ] || _opts=$(myos_envprefix "$_stack" "$_port" allow auth deny prepend proto register strip)
_uris=
for _k in "$@"; do
_v=$(myos_var "${_u}_SERVICE_${_port}_${_k}"); [ -n "$_v" ] && _uris="${_uris:+$_uris }$_v"
done
[ -n "$_uris" ] || _uris=$(myos_var "${_u}_SERVICE_${_port}_URIS")
[ -n "$_uris" ] || _uris=$(myos_uri "$_stack" "$_port")
myos_urlprefix "$_path" "$_opts" "$_uris"
}
+3 -7
View File
@@ -43,7 +43,7 @@ DOCKER_COMPOSE_RUN_WORKDIR ?= $(if $(DOCKER_COMPOSE_WORKDIR),-w $(DOCKER_CO
DOCKER_COMPOSE_SERVICE_NAME ?= $(subst _,-,$(DOCKER_COMPOSE_PROJECT_NAME))
DOCKER_COMPOSE_UP_OPTIONS ?= -d
DOCKER_IMAGE_TAG ?= $(if $(filter true,$(DEPLOY)),$(if $(filter $(ENV),$(ENV_DEPLOY)),$(VERSION)),$(if $(DRONE_BUILD_NUMBER),$(DRONE_BUILD_NUMBER),latest))
DOCKER_IMAGES ?= $(patsubst %/,%,$(patsubst docker/%,%,$(dir $(wildcard docker/*/Dockerfile))))
DOCKER_IMAGES ?= $(foreach dir,$(DOCKER_DIR),$(patsubst $(dir)/%/Dockerfile,%,$(wildcard $(dir)/*/Dockerfile)))
DOCKER_PLUGIN ?= rexray/s3fs:latest
DOCKER_PLUGIN_ARGS ?= $(foreach var,$(DOCKER_PLUGIN_VARS),$(if $(DOCKER_PLUGIN_$(var)),$(var)='$(DOCKER_PLUGIN_$(var))'))
DOCKER_PLUGIN_OPTIONS ?= --grant-all-permissions
@@ -109,11 +109,7 @@ define docker-compose
$(if $(COMPOSE_FILE),
$(if $(DOCKER_COMPOSE),
$(call env-exec,$(RUN) $(DOCKER_COMPOSE) $(DOCKER_COMPOSE_ARGS) $(patsubst %,-f %,$(COMPOSE_FILE)) -p $(COMPOSE_PROJECT_NAME) $(1))
, $(if $(DOCKER_RUN),
$(call docker-build,$(call docker-path,compose),docker/compose,$(COMPOSE_VERSION))
$(call docker-run,docker/compose:$(COMPOSE_VERSION) $(DOCKER_COMPOSE_ARGS),$(patsubst %,-f %,$(COMPOSE_FILE)) -p $(COMPOSE_PROJECT_NAME) $(1))
, $(call env-exec,$(RUN) docker-compose $(DOCKER_COMPOSE_ARGS) $(patsubst %,-f %,$(COMPOSE_FILE)) -p $(COMPOSE_PROJECT_NAME) $(1))
)
, $(call ERROR,docker compose >= $(COMPOSE_VERSION) not found: install the docker compose plugin or docker-compose)
)
)
endef
@@ -156,7 +152,7 @@ define docker-stack-update
$(eval stack_update := $(patsubst %.yml,%,$(notdir $(1))))
$(eval stack_name := $(firstword $(subst :, ,$(stack_update))))
$(eval stack_version := $(or $(2),$(if $(findstring :,$(stack_update)),$(lastword $(subst :, ,$(stack_update))),latest)))
$(eval stack_path := $(patsubst %/,%,$(or $(3),$(realpath $(foreach stack_dir,$(STACK_DIR),$(if $(findstring /,$(1)),$(if $(wildcard $(stack_dir)/$(1) $(stack_dir)/$(1).yml),$(stack_dir)/$(if $(findstring .yml,$(1)),$(dir $(1)),$(if $(wildcard $(stack_dir)/$(1).yml),$(dir $(1)),$(1))),$(if $(wildcard $(stack_dir)/$(stackz)/$(1) $(stack_dir)/$(stackz)/$(1).yml),$(stack_dir)/$(stackz)/$(if $(findstring .yml,$(1)),$(dir $(1)),$(if $(wildcard $(stack_dir)/$(stackz)/$(1).yml),$(dir $(1)),$(1))),$(dir $(1)))))),$(foreach stack_dir,$(STACK_DIR),$(firstword $(wildcard $(stack_dir)/$(stackz)/$(stack_name) $(stack_dir)/$(stackz) $(stack_dir)/$(stack_name))))))))
$(eval stack_path := $(patsubst %/,%,$(or $(3),$(realpath $(foreach stack_dir,$(STACK_DIR),$(if $(findstring /,$(1)),$(if $(wildcard $(stack_dir)/$(1) $(stack_dir)/$(1).yml),$(stack_dir)/$(if $(findstring .yml,$(1)),$(dir $(1)),$(if $(wildcard $(stack_dir)/$(1).yml),$(dir $(1)),$(1))),$(if $(wildcard $(stack_dir)/$(stackz)/$(1) $(stack_dir)/$(stackz)/$(1).yml),$(stack_dir)/$(stackz)/$(if $(findstring .yml,$(1)),$(dir $(1)),$(if $(wildcard $(stack_dir)/$(stackz)/$(1).yml),$(dir $(1)),$(1))),$(dir $(1))))))),$(realpath $(foreach stack_dir,$(STACK_DIR),$(firstword $(wildcard $(stack_dir)/$(stackz)/$(stack_name) $(stack_dir)/$(stackz) $(stack_dir)/$(stack_name))))))))
$(call debug,stack_path)
$(call compose-file,$(stack_path),docker-compose $(stack_name),$(COMPOSE_FILE_SUFFIX) $(stack_version))
$(if $(wildcard $(stack_path)/.env.dist),$(call .env,,$(stack_path)/.env.dist,$(wildcard $(CONFIG)/$(ENV)/$(APP)/.env $(stack_path)/.env.$(ENV) .env)))
+6 -12
View File
@@ -1,9 +1,9 @@
##
# DOCKER
# target docker-build: Fire docker-image-myos, Call docker-build-% target for each DOCKER_IMAGES
# target docker-build: Call docker-build-% target for each DOCKER_IMAGES
.PHONY: docker-build
docker-build: docker-image-myos
docker-build:
$(foreach image,$(or $(SERVICE),$(DOCKER_IMAGES)),$(call make,docker-build-$(image)))
# target docker-build-%: Call docker-build for each Dockerfile in docker/% folder
@@ -27,10 +27,10 @@ docker-commit: stack
docker-commit-%: stack
$(foreach service,$(or $(SERVICE),$(SERVICES)),$(call docker-commit,$(service),,,$*))
# target docker-compose-build: Fire docker-image-myos, Call docker-compose build SERVICE
# target docker-compose-build: Call docker-compose build SERVICE
.PHONY: docker-compose-build
docker-compose-build: DOCKER_RUN_OPTIONS += -it
docker-compose-build: docker-image-myos stack
docker-compose-build: stack
$(call docker-compose,build $(DOCKER_BUILD_ARGS) $(if $(filter $(SERVICE),$(SERVICES)),$(SERVICE)))
# target docker-compose-config: Call docker-compose config
@@ -110,18 +110,12 @@ docker-compose-start: stack
docker-compose-stop: stack
$(call docker-compose,stop $(if $(filter $(SERVICE),$(SERVICES)),$(SERVICE)))
# target docker-compose-up: Fire docker-image-myos, Call docker-compose up SERVICE
# target docker-compose-up: Call docker-compose up SERVICE
.PHONY: docker-compose-up
docker-compose-up: DOCKER_RUN_OPTIONS += -it
docker-compose-up: docker-image-myos bootstrap-stack stack
docker-compose-up: bootstrap-stack stack
$(call docker-compose,up $(DOCKER_COMPOSE_UP_OPTIONS) $(if $(filter $(SERVICE),$(SERVICES)),$(SERVICE)))
# target docker-image-myos: Call myos-docker-build-% target for each MYOS_DOCKER_IMAGES
.PHONY: docker-image-myos
docker-image-myos: MAKE_VARS += DOCKER_REPOSITORY STACK
docker-image-myos:
$(foreach image,$(subst $(quote),,$(MYOS_DOCKER_IMAGES)),$(call make,docker-build-$(image),$(MYOS)))
# target docker-image-rm: Remove docker images matching DOCKER_REPOSITORY
.PHONY: docker-image-rm
docker-image-rm:
+1 -1
View File
@@ -37,7 +37,7 @@ HOST_DOCKER_VOLUME ?= $(HOST_COMPOSE_PROJECT_NAME)
HOST_GID ?= $(HOST_UID)
HOST_UID ?= 123
HOST_STACK ?= $(filter host,$(firstword $(subst /, ,$(STACK))))
MYOS_STACK ?= $(wildcard $(foreach stack_dir,$(STACK_DIR),$(stack_dir)/myos))
MYOS_STACK ?= $(if $(filter . myos,$(MYOS)),$(realpath $(MYOS)/share/compose))
MYOS_STACK_FILE ?= networks volumes
RESU_DOCKER_REPOSITORY ?= $(subst -,/,$(USER_COMPOSE_PROJECT_NAME))
SHARE_DIR ?= . .. ~/.local/share /usr/local/share /usr/share
+1 -1
View File
@@ -19,7 +19,7 @@ include $(filter-out $(wildcard $(MAKE_FILE) $(MAKE_FIRST) $(MAKE_LATEST)),$(wil
## it includes $(MAKE_DIR)/$(MAKE_SUBDIRS)/*.mk
include $(foreach subdir,$(MAKE_SUBDIRS),$(filter-out $(wildcard $(MAKE_DIR)/$(subdir)/def.mk $(MAKE_DIR)/$(subdir)/def.*.mk),$(wildcard $(MAKE_DIR)/$(subdir)/*.mk)))
## if not in $(MYOS) nor $(MONOREPO), it includes def.mk def.*.mk */def.mk */def.*.mk *.mk */*.mk
include $(if $(filter-out . myos,$(MYOS)),$(wildcard def.mk def.*.mk */def.mk */def.*.mk) $(filter-out $(wildcard def.mk def.*.mk */def.mk */def.*.mk stack/*.mk),$(wildcard *.mk */*.mk)))
include $(if $(filter-out . myos,$(MYOS)),$(wildcard def.mk def.*.mk */def.mk */def.*.mk) $(filter-out $(wildcard def.mk def.*.mk */def.mk */def.*.mk $(STACK_DIR_NAME)/*.mk),$(wildcard *.mk */*.mk)))
## it includes $(STACK_DIR)/*.mk $(STACK_DIR)/*/*.mk
include $(foreach stack_dir,$(STACK_DIR),$(wildcard $(stack_dir)/*.mk $(stack_dir)/*/*.mk))
## it includes $(MAKE_LATEST)
+2
View File
@@ -0,0 +1,2 @@
APP_SERVICE_8080_TAGS=urlprefix-app.${DOMAIN}/*
POSTGRES_PASSWORD=changeme
+2
View File
@@ -0,0 +1,2 @@
MYOS ?= /usr/local/lib/myos
-include $(MYOS)/make/include.mk
+4
View File
@@ -0,0 +1,4 @@
services:
app:
environment:
DEBUG: "true"
+13
View File
@@ -0,0 +1,13 @@
services:
app:
image: alpine:3.20
command: sleep infinity
environment:
APP_DOMAIN: ${APP_DOMAIN}
DOMAIN: ${DOMAIN}
labels:
- SERVICE_8080_NAME=${COMPOSE_SERVICE_NAME}-app-8080
- SERVICE_8080_TAGS=${APP_SERVICE_8080_TAGS:-urlprefix-app.localhost/*}
networks:
- default
- private
+5
View File
@@ -0,0 +1,5 @@
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+3
View File
@@ -0,0 +1,3 @@
services:
web:
image: nginx:alpine
@@ -0,0 +1,6 @@
services:
myos:
image: ${USER_DOCKER_IMAGE:-myos:latest}
container_name: ${USER_COMPOSE_PROJECT_NAME:-user}
volumes:
- ${USER_DOCKER_VOLUME:-user}:/tmp/ssh-agent
+1
View File
@@ -0,0 +1 @@
default ?= postgres redis
@@ -0,0 +1,3 @@
services:
drone:
image: drone/drone:1.6
@@ -0,0 +1,2 @@
DRONE_SERVER_HOST ?= drone.$(DOMAIN)
ENV_VARS += DRONE_SERVER_HOST
@@ -0,0 +1,7 @@
services:
drone:
image: drone/drone:${DRONE_VERSION:-latest}
environment:
DRONE_SERVER_HOST: ${DRONE_SERVER_HOST:-drone.localhost}
networks:
- private
@@ -0,0 +1,4 @@
services:
nginx:
volumes:
- dns:/dns
@@ -0,0 +1,4 @@
services:
nginx:
volumes:
- www:/var/www
@@ -0,0 +1,6 @@
services:
nginx:
image: nginx:alpine
container_name: ${HOST_COMPOSE_PROJECT_NAME:-localhost}-nginx
ports:
- "8080:80"
@@ -0,0 +1,3 @@
services:
postgres:
image: postgres:9.6
@@ -0,0 +1,4 @@
services:
postgres:
ports:
- "5432:5432"
@@ -0,0 +1,13 @@
services:
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
labels:
- SERVICE_5432_NAME=${COMPOSE_SERVICE_NAME}-postgres-5432
networks:
- private
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
@@ -0,0 +1,5 @@
services:
redis:
image: redis:alpine
networks:
- private
+1
View File
@@ -0,0 +1 @@
testing ?= drone/drone redis
+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
+10
View File
@@ -0,0 +1,10 @@
# Stack host: consul + fabio (80/443) + registrator, one instance per host.
host ?= host/consul host/fabio host/registrator
.PHONY: host
host:
$(call make,up STACK="$(host)")
.PHONY: host-down
host-down:
$(call make,down STACK="$(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
+35
View File
@@ -0,0 +1,35 @@
# Intentional differences between the legacy make engine and the bash CLI
Golden expectations in `expected/` are recorded from the legacy engine
(git tag `legacy-1.0-beta`). When the CLI intentionally behaves differently the
case gets an override in `expected.cli/<case>.txt` and a line here.
`make test-golden MYOS_ENGINE=cli` checks the CLI against those overrides.
## Shape of the output
| what | legacy | cli |
|---|---|---|
| sub-make line | every command is preceded by the recursive `make ... docker-compose-up STACK=...` it re-enters | no recursion, so no such line |
| compose invocation | `docker --log-level=error compose --ansi=auto -f ... -p ...` | `docker compose -f ... -p ... --project-directory <dir of the first file>` |
| several stacks in one project | one `docker compose up` per stack, each with the full file list, so N stacks meant N identical calls | a single call per project |
| network creation | `sh -c docker network create <n> >/dev/null 2>&1` on every up | `docker network create <n>`, only when the network is missing |
| unknown command | prints a warning and exits 0 | prints an error and exits 2 |
| unknown stack | silently resolves to nothing and exits 0 | exits 3 with the search path |
## Values
| what | legacy | cli | why |
|---|---|---|---|
| `APP` in wrapper mode | `myos`, the framework directory itself | the stack, or the directory being run | the legacy value described the framework, not the thing being deployed. The real `up` path already passed `APP_NAME=<stack>` to its sub-make, so the CLI matches what actually ran; only the `print-` target disagreed. |
| `COMPOSE_PROJECT_NAME` for a catalogue stack in wrapper mode | `<user>-myos-<env>` for every stack | `<user>-<env>-<stack>` | same reason: two different stacks shared one project name when printed, but not when run |
| `COMPOSE_PROJECT_NAME` default order | `<user>-<app>-<env>` | `<user>-<env>-<app>` | environments are shared across apps, so the environment reads better in the middle. `MYOS_PROJECT_FORMAT=user-app-env` restores the old order and is what `myos migrate pin` writes into existing deployments. |
| compose files of the current directory | ignored in wrapper mode: only a `STACK` was ever loaded | loaded, as a stack named after the directory | this is the point of unifying the two modes |
| overlay order for several suffixes | the order the `COMPOSE_FILE_*` variables happened to be declared in | sorted by suffix name | a compose overlay wins over the ones before it, so the order has to be predictable rather than depend on where a variable was set |
| framework overlays | `stack/myos/*.yml` | `share/compose/*.yml` | the catalogue moved to myos-stacks; these files stay with the framework |
## Tag helpers
| case | delta | why |
|---|---|---|
| `myos_urlprefix` with several uris | the bash port joins tags with `,`, the make macro emits `tag ,tag` | the make template ends with ` $(2)` (options), so an empty option list leaves a space before the comma. Harmless but sloppy; every tag written by hand on the fleet uses the clean form. |
| `myos_urlprefix` with options passed inside the path argument | the bash port keeps `*` right after the path (`urlprefix-host:443/* proto=https`), the make macro appends it after the options (`urlprefix-host:443/ proto=https*`) | only reachable by stuffing options into argument 1, which the stale example in `make/apps/def.mk` did. Through `tagprefix`, the real code path, both engines agree. |
+72
View File
@@ -0,0 +1,72 @@
# name | fixture | args
#
# `help` is deliberately absent: it renders the comments of the Makefile, so it
# would break on every edit to a dev target rather than on a change of behaviour.
host-print-project | host-project | print-COMPOSE_PROJECT_NAME STACK=host/consul
host-print-compose-file | host-project | print-COMPOSE_FILE STACK=host/consul
host-print-stack-dir | host-project | print-STACK_DIR STACK=host/consul
host-print-env-vars | host-project | print-ENV_VARS STACK=host/consul
host-print-suffix | host-project | print-COMPOSE_FILE_SUFFIX STACK=host/consul
host-print-network-default | host-project | print-DOCKER_NETWORK_DEFAULT STACK=host/consul
host-print-network-private | host-project | print-DOCKER_NETWORK_PRIVATE STACK=host/consul
host-print-network-public | host-project | print-DOCKER_NETWORK_PUBLIC STACK=host/consul
host-print-host-stack | host-project | print-HOST_STACK STACK=host/consul
host-print-app | host-project | print-APP STACK=host/consul
host-up-consul | host-project | up STACK=host/consul
host-up-two | host-project | up STACK="host/consul host/fabio"
host-up-group | host-project | up STACK=host
host-target | host-project | host
host-down-group | host-project | down STACK=host
host-config | host-project | config STACK=host/fabio
host-ps | host-project | ps STACK=host/consul
host-logs | host-project | logs STACK=host/consul
host-stack-host-config | host-project | stack-host-config
host-exec | host-project | exec STACK=host/consul SERVICE=consul ARGS='consul members'
host-env-master | host-project | print-COMPOSE_FILE STACK=host/consul ENV=master
host-print-project-env-master | host-project | print-COMPOSE_PROJECT_NAME STACK=host/consul ENV=master
app-print-project | app-git | print-COMPOSE_PROJECT_NAME
app-print-compose-file | app-git | print-COMPOSE_FILE
app-print-app | app-git | print-APP
app-print-stack | app-git | print-STACK
app-up | app-git | up
app-up-env-master | app-git | up ENV=master
app-config | app-git | config
app-down | app-git | down
app-run | app-git | run SERVICE=app ARGS='id'
app-scale | app-git | scale SERVICE=app NUM=2
app-print-service-name | app-git | print-COMPOSE_SERVICE_NAME
app-print-repository | app-git | print-DOCKER_REPOSITORY
nogit-print-project | app-nogit | print-COMPOSE_PROJECT_NAME
nogit-print-app | app-nogit | print-APP
nogit-up | app-nogit | up
cat-postgres-compose-file | app-nogit | print-COMPOSE_FILE STACK=postgres
cat-postgres-project | app-nogit | print-COMPOSE_PROJECT_NAME STACK=postgres
cat-postgres-up | app-nogit | up STACK=postgres
cat-postgres-version | app-nogit | print-COMPOSE_FILE STACK=postgres:9.6
cat-postgres-env-master | app-nogit | print-COMPOSE_FILE STACK=postgres ENV=master
cat-user-project | app-nogit | print-COMPOSE_PROJECT_NAME STACK=User/User
cat-user-compose-file | app-nogit | print-COMPOSE_FILE STACK=User/User
cat-user-network | app-nogit | print-DOCKER_NETWORK STACK=User/User
cat-group-testing | app-nogit | print-COMPOSE_FILE STACK=testing
cat-group-testing-up | app-nogit | up STACK=testing
cat-group-default-up | app-nogit | up STACK=default
cat-drone-version | app-nogit | print-COMPOSE_FILE STACK=drone/drone:1.6
cat-drone-env-vars | app-nogit | print-ENV_VARS STACK=drone/drone
cat-nginx-www | app-nogit | print-COMPOSE_FILE STACK=host/nginx COMPOSE_FILE_WWW=true
cat-nginx-www-dns | app-nogit | print-COMPOSE_FILE STACK=host/nginx COMPOSE_FILE_WWW=true COMPOSE_FILE_DNS=true
cat-nginx-project | app-nogit | print-COMPOSE_PROJECT_NAME STACK=host/nginx
cat-unknown-stack | app-nogit | print-COMPOSE_FILE STACK=doesnotexist
cat-unknown-target | app-nogit | doesnotexist
inc-app-print-project | app-git | @make print-COMPOSE_PROJECT_NAME
inc-app-print-compose-file | app-git | @make print-COMPOSE_FILE
inc-app-print-app | app-git | @make print-APP
inc-app-up | app-git | @make up
inc-app-up-env-master | app-git | @make up ENV=master
inc-app-config | app-git | @make config
inc-app-down | app-git | @make down
inc-app-ps | app-git | @make ps
inc-app-print-service-name | app-git | @make print-COMPOSE_SERVICE_NAME
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-stack-postgres-up | app-git | @make up STACK=postgres
inc-app-unknown-target | app-git | @make doesnotexist
+2
View File
@@ -0,0 +1,2 @@
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@ config
[exit 0]
+2
View File
@@ -0,0 +1,2 @@
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@ down
[exit 0]
@@ -0,0 +1,2 @@
APP wd
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_FILE @WD@/docker-compose.yml @WD@/docker-compose.local.yml @WD@/docker/docker-compose.yml @MYOS@/share/compose/networks.yml
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_PROJECT_NAME tester-wd-local
[exit 0]
@@ -0,0 +1,2 @@
DOCKER_REPOSITORY tester/wd/local
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_SERVICE_NAME tester-wd-local
[exit 0]
@@ -0,0 +1,2 @@
STACK ./
[exit 0]
+26
View File
@@ -0,0 +1,26 @@
ERROR: unknown command: run
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...]
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]
+26
View File
@@ -0,0 +1,26 @@
ERROR: unknown command: scale
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...]
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]
@@ -0,0 +1,4 @@
docker network create tester-master
docker network create testhost
docker compose -f @WD@/docker-compose.yml -f @WD@/docker/docker-compose.yml -f @MYOS@/share/compose/networks.yml -p tester-wd-master --project-directory @WD@ up -d
[exit 0]
+4
View File
@@ -0,0 +1,4 @@
docker network create tester-local
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
[exit 0]
@@ -0,0 +1,2 @@
ENV_VARS
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_FILE @HOME@/.local/share/myos/stack/drone/drone.yml @HOME@/.local/share/myos/stack/drone/drone.1.6.yml @MYOS@/share/compose/networks.yml
[exit 0]
@@ -0,0 +1,7 @@
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
docker network create tester-local
docker network create testhost
docker compose -f @HOME@/.local/share/myos/stack/redis/redis.yml -f @MYOS@/share/compose/networks.yml -p tester-redis-local --project-directory @HOME@/.local/share/myos/stack/redis up -d
[exit 0]
@@ -0,0 +1,7 @@
docker network create tester-local
docker network create testhost
docker compose -f @HOME@/.local/share/myos/stack/drone/drone.yml -f @MYOS@/share/compose/networks.yml -p tester-drone-local --project-directory @HOME@/.local/share/myos/stack/drone up -d
docker network create tester-local
docker network create testhost
docker compose -f @HOME@/.local/share/myos/stack/redis/redis.yml -f @MYOS@/share/compose/networks.yml -p tester-redis-local --project-directory @HOME@/.local/share/myos/stack/redis up -d
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_FILE @HOME@/.local/share/myos/stack/host/nginx.yml @HOME@/.local/share/myos/stack/host/nginx.dns.yml @HOME@/.local/share/myos/stack/host/nginx.www.yml @MYOS@/share/compose/networks.yml @MYOS@/share/compose/volumes.dns.local.yml @MYOS@/share/compose/volumes.www.local.yml
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_PROJECT_NAME tester-postgres-local
[exit 0]
@@ -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
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_FILE @HOME@/.local/share/myos/stack/postgres/postgres.yml @HOME@/.local/share/myos/stack/postgres/postgres.local.yml @HOME@/.local/share/myos/stack/postgres/postgres.9.6.yml @MYOS@/share/compose/networks.yml
[exit 0]
@@ -0,0 +1,26 @@
ERROR: unknown command: doesnotexist
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...]
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
View File
@@ -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 config
[exit 0]
@@ -0,0 +1,2 @@
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 down
[exit 0]
+26
View File
@@ -0,0 +1,26 @@
ERROR: unknown command: exec
Usage: myos [options] <command> [stack...] [VAR=value...] [-- args...]
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
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 logs --follow --tail=100
[exit 0]
@@ -0,0 +1,2 @@
APP consul
[exit 0]
@@ -0,0 +1,2 @@
ENV_VARS
[exit 0]
@@ -0,0 +1,2 @@
STACK_DIR
[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 ps
[exit 0]
@@ -0,0 +1,2 @@
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 config
[exit 0]
+2
View File
@@ -0,0 +1,2 @@
ERROR: no stack given, and no compose file in @WD@
[exit 2]
@@ -0,0 +1,4 @@
docker network create tester-local
docker network create testhost
docker compose -f @WD@/stack/host/consul.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host up -d
[exit 0]
@@ -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 @WD@/stack/host/registrator.yml -f @MYOS@/share/compose/networks.yml -p testhost --project-directory @WD@/stack/host up -d
[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]
@@ -0,0 +1,2 @@
APP wd
[exit 0]
@@ -0,0 +1,2 @@
COMPOSE_PROJECT_NAME tester-wd-local
[exit 0]
+4
View File
@@ -0,0 +1,4 @@
docker network create tester-local
docker network create testhost
docker compose -f @WD@/docker-compose.yml -f @MYOS@/share/compose/networks.yml -p tester-wd-local --project-directory @WD@ up -d
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
make -o docker-stack-config MAKE_OLDFILE=docker-stack-config ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-config STACK=myos APP_NAME=myos
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local config
[exit 0]
+3
View File
@@ -0,0 +1,3 @@
make -o docker-stack-down MAKE_OLDFILE=docker-stack-down ENV=local DOCKER_COMPOSE=docker --log-level=error compose docker-compose-down STACK=myos APP_NAME=myos
docker --log-level=error compose --ansi=auto -f @MYOS@/share/compose/networks.yml -p tester-myos-local down
[exit 0]

Some files were not shown because too many files have changed in this diff Show More