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/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; diff --git a/test/docker-compose.test.yml b/test/docker-compose.test.yml index a1a8c9f..8158b76 100644 --- a/test/docker-compose.test.yml +++ b/test/docker-compose.test.yml @@ -15,6 +15,9 @@ 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: @@ -23,7 +26,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 + - ../test/.tls:/opt/fmsg/tls:ro,z + # 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 @@ -49,5 +55,6 @@ services: - "${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..2092a79 --- /dev/null +++ b/test/run-tests-podman.sh @@ -0,0 +1,160 @@ +#!/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. +# - 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 +# `--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 '' + + +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) + + +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(drop_profiles(flatten_depends_on(resolve_vars(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" "$@"