From 787efaf07640dfdce7c8627fdf23bbdb879bdcc9 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Wed, 1 Jul 2026 20:04:39 +0800 Subject: [PATCH 1/4] Add podman-based integration test runner test/run-tests-podman.sh shims docker -> podman and delegates to run-tests.sh, working around several podman-compose (1.6.0) quirks (broken !override variable interpolation, --wait hanging on one-shot containers, hyphen vs underscore container naming). Also fixes issues that affect podman regardless of the wrapper: - add :z SELinux relabeling to read-only bind mounts so rootless podman on enforcing SELinux hosts can read them - fmsg-webapi now creates/chowns /opt/fmsg/data like fmsgd, closing a race where whichever container touches the shared named volume first seeds its ownership - simplify the test compose override's depends_on to short form and drop the certbot profile gate, which podman-compose can't parse/ resolve correctly --- compose/docker-compose.yml | 4 +- docker/fmsg-webapi/Dockerfile | 3 +- test/docker-compose.test.yml | 18 ++---- test/run-tests-podman.sh | 105 ++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 15 deletions(-) create mode 100755 test/run-tests-podman.sh diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index bff8971..a8c3000 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -9,7 +9,7 @@ services: CERTBOT_EMAIL: ${CERTBOT_EMAIL} volumes: - letsencrypt:/etc/letsencrypt - - ../docker/certbot/entrypoint.sh:/entrypoint.sh:ro + - ../docker/certbot/entrypoint.sh:/entrypoint.sh:ro,z ports: - "80:80" @@ -26,7 +26,7 @@ services: FMSGID_READER_PGPASSWORD: ${FMSGID_READER_PGPASSWORD} volumes: - postgres_data:/var/lib/postgresql - - ../docker/postgres/init:/docker-entrypoint-initdb.d:ro + - ../docker/postgres/init:/docker-entrypoint-initdb.d:ro,z healthcheck: test: ["CMD-SHELL", "pg_isready -U ${PGUSER:-postgres}"] interval: 10s diff --git a/docker/fmsg-webapi/Dockerfile b/docker/fmsg-webapi/Dockerfile index 56aa041..32a17d0 100644 --- a/docker/fmsg-webapi/Dockerfile +++ b/docker/fmsg-webapi/Dockerfile @@ -16,7 +16,8 @@ WORKDIR /opt/fmsg-webapi COPY --from=builder /build/fmsg-webapi /opt/fmsg-webapi/fmsg-webapi -RUN chown -R fmsg:fmsg /opt/fmsg-webapi +RUN mkdir -p /opt/fmsg/data && \ + chown -R fmsg:fmsg /opt/fmsg-webapi /opt/fmsg/data USER fmsg diff --git a/test/docker-compose.test.yml b/test/docker-compose.test.yml index a1a8c9f..3dfb1f9 100644 --- a/test/docker-compose.test.yml +++ b/test/docker-compose.test.yml @@ -15,7 +15,6 @@ services: entrypoint: ["true"] restart: "no" ports: !override [] - profiles: ["certbot"] fmsgd: environment: @@ -23,12 +22,10 @@ services: FMSG_TLS_KEY: /opt/fmsg/tls/fmsg.${FMSG_DOMAIN}.key FMSG_TLS_INSECURE_SKIP_VERIFY: "true" volumes: - - ../test/.tls:/opt/fmsg/tls:ro - depends_on: !override - postgres: - condition: service_healthy - fmsgid: - condition: service_started + - ../test/.tls:/opt/fmsg/tls:ro,z + # Short (list) form so this fully replaces the base depends_on (dropping + # certbot, which is stubbed out below) instead of merging with it. + depends_on: !override [postgres, fmsgid] networks: default: fmsg-test: @@ -40,14 +37,11 @@ services: environment: FMSG_TLS_CERT: "" FMSG_TLS_KEY: "" - depends_on: !override - fmsgd: - condition: service_started - fmsgid: - condition: service_started + depends_on: !override [fmsgd, fmsgid] ports: !override - "${FMSG_WEBAPI_HOST_PORT:-8081}:${FMSG_API_PORT:-8000}" networks: + default: fmsg-test: external: true diff --git a/test/run-tests-podman.sh b/test/run-tests-podman.sh new file mode 100755 index 0000000..542981a --- /dev/null +++ b/test/run-tests-podman.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# ============================================================= +# Run the fmsg integration tests using podman instead of docker. +# +# Prerequisites: podman, podman-compose (or docker-compose) go (1.24+), curl +# +# This shims a `docker` executable onto PATH that execs podman, then +# delegates straight to run-tests.sh so the two runners stay in sync. +# +# podman-compose (as of 1.6.0) has a couple of issues this shim works around: +# - values under a YAML `!override` tag are not passed through variable +# interpolation, so `${VAR:-default}` references inside an `!override` +# block reach podman literally. The shim pre-resolves those itself. +# - `up --wait` runs `podman wait --condition=running` for every service +# without a healthcheck, including one-shot containers (like certbot +# here) that are expected to exit — so it hangs forever. The shim strips +# `--wait` from `compose up`; run-tests.sh already polls the webapi HTTP +# endpoint before proceeding, so readiness is still verified. +# +# podman-compose also names containers "__" +# (underscores) whereas docker compose v2 names them +# "--" (hyphens). run-tests.sh hardcodes the +# docker-compose-v2-style names for `docker exec` targets, so the shim +# rewrites those to podman-compose's naming for that subcommand only. +# +# Usage: same as run-tests.sh, e.g. +# ./test/run-tests-podman.sh +# ./test/run-tests-podman.sh --no-start +# ./test/run-tests-podman.sh cleanup +# ============================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +command -v podman &>/dev/null || { echo "podman not found on PATH" >&2; exit 1; } + +SHIM_DIR="$(mktemp -d)" +trap 'rm -rf "$SHIM_DIR"' EXIT + +cat > "$SHIM_DIR/resolve-compose-vars.py" <<'EOF' +import os +import re +import sys + +PATTERN = re.compile(r'\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}') + + +def repl(match): + name, _, default = match.groups() + value = os.environ.get(name) + if value: + return value + return default if default is not None else '' + + +text = open(sys.argv[1]).read() +sys.stdout.write(PATTERN.sub(repl, text)) +EOF + +cat > "$SHIM_DIR/docker" < "\$resolved" + args[\$next]="\$resolved" + ;; + esac + fi + if [ "\${args[\$i]}" = "--wait" ]; then + continue + fi + filtered+=("\${args[\$i]}") + done + args=("\${filtered[@]}") +fi + +exec podman "\${args[@]}" +EOF +chmod +x "$SHIM_DIR/docker" + +export PATH="$SHIM_DIR:$PATH" + +exec "$SCRIPT_DIR/run-tests.sh" "$@" From 2b5d372adc253ea8809f6f96b9015ba6231d1af8 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Thu, 2 Jul 2026 13:43:14 +0800 Subject: [PATCH 2/4] line endings? --- docker/postgres/init/999-permissions.sql | 98 ++++++++++++------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/docker/postgres/init/999-permissions.sql b/docker/postgres/init/999-permissions.sql index 77a4fcb..c12dab8 100644 --- a/docker/postgres/init/999-permissions.sql +++ b/docker/postgres/init/999-permissions.sql @@ -1,49 +1,49 @@ --- ============================================================= --- Permissions — runs last, after all objects exist. --- ============================================================= - --- ── fmsgd database ────────────────────────────────────────── - -\connect fmsgd - -REVOKE ALL ON DATABASE fmsgd FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM PUBLIC; - --- fmsgd_writer: read/write access -GRANT CONNECT ON DATABASE fmsgd TO fmsgd_writer; -GRANT USAGE ON SCHEMA public TO fmsgd_writer; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgd_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgd_writer; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_writer; - --- fmsgd_reader: read-only access -GRANT CONNECT ON DATABASE fmsgd TO fmsgd_reader; -GRANT USAGE ON SCHEMA public TO fmsgd_reader; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgd_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgd_reader; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_reader; - --- ── fmsgid database ───────────────────────────────────────── - -\connect fmsgid - -REVOKE ALL ON DATABASE fmsgid FROM PUBLIC; -REVOKE ALL ON SCHEMA public FROM PUBLIC; - --- fmsgid_writer: read/write access -GRANT CONNECT ON DATABASE fmsgid TO fmsgid_writer; -GRANT USAGE ON SCHEMA public TO fmsgid_writer; -GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgid_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgid_writer; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_writer; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_writer; - --- fmsgid_reader: read-only access -GRANT CONNECT ON DATABASE fmsgid TO fmsgid_reader; -GRANT USAGE ON SCHEMA public TO fmsgid_reader; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgid_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgid_reader; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_reader; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_reader; +-- ============================================================= +-- Permissions — runs last, after all objects exist. +-- ============================================================= + +-- ── fmsgd database ────────────────────────────────────────── + +\connect fmsgd + +REVOKE ALL ON DATABASE fmsgd FROM PUBLIC; +REVOKE ALL ON SCHEMA public FROM PUBLIC; + +-- fmsgd_writer: read/write access +GRANT CONNECT ON DATABASE fmsgd TO fmsgd_writer; +GRANT USAGE ON SCHEMA public TO fmsgd_writer; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgd_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgd_writer; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_writer; + +-- fmsgd_reader: read-only access +GRANT CONNECT ON DATABASE fmsgd TO fmsgd_reader; +GRANT USAGE ON SCHEMA public TO fmsgd_reader; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgd_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgd_reader; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgd_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgd_reader; + +-- ── fmsgid database ───────────────────────────────────────── + +\connect fmsgid + +REVOKE ALL ON DATABASE fmsgid FROM PUBLIC; +REVOKE ALL ON SCHEMA public FROM PUBLIC; + +-- fmsgid_writer: read/write access +GRANT CONNECT ON DATABASE fmsgid TO fmsgid_writer; +GRANT USAGE ON SCHEMA public TO fmsgid_writer; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fmsgid_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fmsgid_writer; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_writer; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_writer; + +-- fmsgid_reader: read-only access +GRANT CONNECT ON DATABASE fmsgid TO fmsgid_reader; +GRANT USAGE ON SCHEMA public TO fmsgid_reader; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO fmsgid_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO fmsgid_reader; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fmsgid_reader; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO fmsgid_reader; From 53965d4327618e108498688faef0dd2cb0e0c289 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Thu, 2 Jul 2026 14:45:01 +0800 Subject: [PATCH 3/4] Fix docker CI regression from podman depends_on simplification Collapsing fmsgd/fmsg-webapi depends_on to short-list form dropped the postgres service_healthy condition, letting fmsgd race postgres on startup under Docker Compose. Restore the long form with conditions in the shared compose file, and have the podman shim flatten it back to a short list (preserving the !override tag, which the previous flattening dropped and which let podman-compose merge in a stale certbot dependency, causing an intermittent "container state improper" failure on the second stack). --- test/docker-compose.test.yml | 17 +++++++++++---- test/run-tests-podman.sh | 41 +++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/test/docker-compose.test.yml b/test/docker-compose.test.yml index 3dfb1f9..31b2d6d 100644 --- a/test/docker-compose.test.yml +++ b/test/docker-compose.test.yml @@ -23,9 +23,14 @@ services: FMSG_TLS_INSECURE_SKIP_VERIFY: "true" volumes: - ../test/.tls:/opt/fmsg/tls:ro,z - # Short (list) form so this fully replaces the base depends_on (dropping - # certbot, which is stubbed out below) instead of merging with it. - depends_on: !override [postgres, fmsgid] + # Fully replaces the base depends_on (dropping certbot, which is stubbed + # out below) instead of merging with it, while still waiting on + # postgres's healthcheck so fmsgd doesn't race it on startup. + depends_on: !override + postgres: + condition: service_healthy + fmsgid: + condition: service_started networks: default: fmsg-test: @@ -37,7 +42,11 @@ services: environment: FMSG_TLS_CERT: "" FMSG_TLS_KEY: "" - depends_on: !override [fmsgd, fmsgid] + depends_on: !override + fmsgd: + condition: service_started + fmsgid: + condition: service_started ports: !override - "${FMSG_WEBAPI_HOST_PORT:-8081}:${FMSG_API_PORT:-8000}" diff --git a/test/run-tests-podman.sh b/test/run-tests-podman.sh index 542981a..418b459 100755 --- a/test/run-tests-podman.sh +++ b/test/run-tests-podman.sh @@ -53,8 +53,47 @@ def repl(match): return default if default is not None else '' +def resolve_vars(text): + return PATTERN.sub(repl, text) + + +# podman-compose (1.6.0) can't parse the long (mapping) form of `depends_on` +# with `condition:` entries under an `!override` tag, so collapse it to the +# short list form it does understand. Docker Compose keeps using the +# original file with the healthcheck conditions intact. +DEPENDS_ON_RE = re.compile(r'^(\s*)depends_on:\s*!override\s*$') +KEY_RE = re.compile(r'^(\s+)([A-Za-z0-9_.-]+):\s*$') +CONDITION_RE = re.compile(r'^\s+condition:\s*\S+\s*$') + + +def flatten_depends_on(text): + lines = text.split('\n') + out = [] + i = 0 + while i < len(lines): + m = DEPENDS_ON_RE.match(lines[i]) + if not m: + out.append(lines[i]) + i += 1 + continue + base_indent = m.group(1) + i += 1 + keys = [] + while i < len(lines): + km = KEY_RE.match(lines[i]) + if km and len(km.group(1)) > len(base_indent): + keys.append(km.group(2)) + i += 1 + while i < len(lines) and CONDITION_RE.match(lines[i]): + i += 1 + continue + break + out.append('{}depends_on: !override [{}]'.format(base_indent, ', '.join(keys))) + return '\n'.join(out) + + text = open(sys.argv[1]).read() -sys.stdout.write(PATTERN.sub(repl, text)) +sys.stdout.write(flatten_depends_on(resolve_vars(text))) EOF cat > "$SHIM_DIR/docker" < Date: Thu, 2 Jul 2026 15:05:36 +0800 Subject: [PATCH 4/4] Restore certbot profile gate to fix docker CI --wait failure The podman commit dropped `profiles: ["certbot"]` from the stubbed certbot service to work around a podman-compose parse issue. Without the profile gate, `docker compose up` starts certbot by default, and since its stub entrypoint exits immediately by design, `--wait` treats that expected exit as an unexpected failure and aborts the whole stack start-up. Restore the profile gate for Docker Compose and have the podman shim strip it, since podman-compose can't parse it and doesn't need it (the shim already drops --wait entirely). --- test/docker-compose.test.yml | 4 ++++ test/run-tests-podman.sh | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/test/docker-compose.test.yml b/test/docker-compose.test.yml index 31b2d6d..8158b76 100644 --- a/test/docker-compose.test.yml +++ b/test/docker-compose.test.yml @@ -15,6 +15,10 @@ services: entrypoint: ["true"] restart: "no" ports: !override [] + # Gate certbot behind a profile that's never activated so `compose up` + # never starts it at all. Without this, Docker Compose's `--wait` treats + # the stub's intentional immediate exit as an unexpected failure. + profiles: ["certbot"] fmsgd: environment: diff --git a/test/run-tests-podman.sh b/test/run-tests-podman.sh index 418b459..2092a79 100755 --- a/test/run-tests-podman.sh +++ b/test/run-tests-podman.sh @@ -11,6 +11,15 @@ # - values under a YAML `!override` tag are not passed through variable # interpolation, so `${VAR:-default}` references inside an `!override` # block reach podman literally. The shim pre-resolves those itself. +# - the long (mapping) form of `depends_on` with `condition:` entries under +# an `!override` tag isn't parsed correctly, so the shim collapses it to +# the short list form podman-compose understands (Docker Compose keeps +# using the file as-is, conditions intact). +# - `profiles:` under an `!override` tag also isn't parsed correctly, so +# the shim strips it. This is harmless for podman: the shim already +# strips `--wait` (see below), so certbot's stub exiting immediately +# doesn't need to be gated behind an inactive profile the way it does +# for Docker Compose. # - `up --wait` runs `podman wait --condition=running` for every service # without a healthcheck, including one-shot containers (like certbot # here) that are expected to exit — so it hangs forever. The shim strips @@ -92,8 +101,15 @@ def flatten_depends_on(text): return '\n'.join(out) +PROFILES_RE = re.compile(r'^\s*profiles:\s*\[.*\]\s*$') + + +def drop_profiles(text): + return '\n'.join(line for line in text.split('\n') if not PROFILES_RE.match(line)) + + text = open(sys.argv[1]).read() -sys.stdout.write(flatten_depends_on(resolve_vars(text))) +sys.stdout.write(drop_profiles(flatten_depends_on(resolve_vars(text)))) EOF cat > "$SHIM_DIR/docker" <