# =============================================================================
# certctl Testing Environment — Docker Compose
# =============================================================================
#
# Spins up the full certctl platform with real CA backends for manual QA:
#
#   0. certctl-tls-init     — one-shot init container; writes self-signed
#                             server.crt/.key/ca.crt into ./test/certs (bind
#                             mount, not a named volume — host-readable for
#                             the Go integration test binary)
#   1. PostgreSQL 16        — database (clean, no demo data)
#   2. certctl-server       — control plane API + web dashboard on :8443 (HTTPS)
#   3. certctl-agent        — polls for work, deploys certs to NGINX
#   4. step-ca              — private CA (JWK provisioner, auto-bootstraps)
#   5. Pebble               — ACME test server (simulates Let's Encrypt)
#   6. pebble-challtestsrv  — DNS/HTTP challenge test server for Pebble
#   7. NGINX                — TLS target server on :8080 (HTTP) / :8444 (HTTPS)
#
# Usage:
#   cd deploy
#   docker compose -f docker-compose.test.yml up --build
#
# Dashboard:  https://localhost:8443   (self-signed — use --cacert test/certs/ca.crt)
# API key:    test-key-2026
# NGINX:      https://localhost:8444 (self-signed placeholder until cert deployed)
#
# Integration tests: `go test -tags integration ./deploy/test/...` picks up
# the CA bundle at ./test/certs/ca.crt automatically via CERTCTL_TEST_CA_BUNDLE.
#
# See docs/test-env.md for the full walkthrough.
# =============================================================================

services:

  # ---------------------------------------------------------------------------
  # HTTPS-Everywhere Phase 6 — self-signed TLS bootstrap for the test harness.
  # ---------------------------------------------------------------------------
  # Mirrors the production `certctl-tls-init` (see docker-compose.yml §10-43)
  # but writes into a *host bind mount* (./test/certs) instead of a named
  # volume. The named-volume approach works fine inside Docker but hides the
  # CA bundle from the Go integration test binary that runs on the host; the
  # bind mount exposes /etc/certctl/tls/ca.crt at deploy/test/certs/ca.crt
  # so `newTestClient()` can load it into an x509.CertPool and validate the
  # self-signed server cert. Test-only divergence, explicitly documented.
  #
  # The generated cert has SAN=DNS:certctl-server,DNS:localhost,IP:127.0.0.1
  # so both in-cluster traffic (agent → certctl-server:8443) and host traffic
  # (go test → localhost:8443) validate cleanly. Destroy via
  # `docker compose -f docker-compose.test.yml down -v` + `rm -rf test/certs`
  # to force regeneration. Keys written 0600, certs 0644, owned 1000:1000
  # (the UID the server binary runs as inside its container per Dockerfile:64).
  certctl-tls-init:
    image: alpine/openssl:latest
    container_name: certctl-test-tls-init
    restart: "no"
    entrypoint: /bin/sh
    command:
      - -c
      - |
        set -eu
        CERT=/etc/certctl/tls/server.crt
        KEY=/etc/certctl/tls/server.key
        CA=/etc/certctl/tls/ca.crt
        if [ -f "$$CERT" ] && [ -f "$$KEY" ] && [ -f "$$CA" ]; then
          echo "TLS cert already present at $$CERT — skipping generation"
        else
          mkdir -p /etc/certctl/tls
          openssl req -x509 -newkey ec \
            -pkeyopt ec_paramgen_curve:P-256 \
            -nodes \
            -keyout "$$KEY" \
            -out "$$CERT" \
            -days 3650 \
            -subj "/CN=certctl-server" \
            -addext "subjectAltName=DNS:certctl-server,DNS:localhost,IP:127.0.0.1,IP:::1"
          cp "$$CERT" "$$CA"
          echo "Generated self-signed TLS cert for certctl-test-server (ECDSA-P256/SHA-256, 3650d, CN=certctl-server)"
        fi
        # The test server container runs as root (see `user: "0:0"` below)
        # because setup-trust.sh needs to update the system trust store, so
        # the perms here are really about host-side readability — 0644 on
        # the CA/cert lets `go test` on the host read the bundle without a
        # chown dance.
        chown 1000:1000 "$$CERT" "$$KEY" "$$CA" || true
        chmod 0644 "$$CERT" "$$CA"
        chmod 0600 "$$KEY"
    volumes:
      - ./test/certs:/etc/certctl/tls
    networks:
      certctl-test:
        ipv4_address: 10.30.50.9

  # ---------------------------------------------------------------------------
  # Database
  # ---------------------------------------------------------------------------
  #
  # U-3 (P1, cat-u-seed_initdb_schema_drift, GitHub #10): the test stack used
  # to mount a hand-curated subset of migrations + seed.sql + a never-checked-in
  # seed_test.sql into postgres `/docker-entrypoint-initdb.d/`. Same hazard as
  # the production compose — initdb crashed any time a new migration shipped
  # that the seed depended on without the mount list being updated. Post-U-3
  # the schema is built EXCLUSIVELY by the server at startup via
  # internal/repository/postgres.RunMigrations + RunSeed. Postgres comes up
  # empty and the server lands the full ladder + baseline seed in one shot.
  # `start_period: 30s` matches the production compose and shields slow CI
  # runners from healthcheck flap during initdb.
  postgres:
    image: postgres:16-alpine
    container_name: certctl-test-postgres
    environment:
      POSTGRES_DB: certctl
      POSTGRES_USER: certctl
      POSTGRES_PASSWORD: testpass
    volumes:
      - test_postgres_data:/var/lib/postgresql/data
    networks:
      certctl-test:
        ipv4_address: 10.30.50.2
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U certctl -d certctl"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

  # ---------------------------------------------------------------------------
  # Pebble — ACME test server (simulates Let's Encrypt)
  # ---------------------------------------------------------------------------
  # Pebble is the official ACME test server from Let's Encrypt (RFC 8555).
  # It validates challenges via the companion challtestsrv.
  # Root CA cert available at https://pebble:15000/roots/0 (management API).
  pebble-challtestsrv:
    image: ghcr.io/letsencrypt/pebble-challtestsrv:latest
    container_name: certctl-test-challtestsrv
    # ENTRYPOINT is /app (the binary). command: provides only the FLAGS.
    # Matches the official Pebble docker-compose format.
    # -doh "" disables DoH (default :8443 would conflict with certctl server).
    # defaultIPv4 must point to the certctl-server (10.30.50.6) because that's where
    # the ACME HTTP-01 challenge server runs (port 80 inside the container).
    # Pebble resolves domains via challtestsrv, then connects to this IP to validate.
    command: -defaultIPv4 10.30.50.6 -defaultIPv6 "" -doh ""
    networks:
      certctl-test:
        ipv4_address: 10.30.50.3
    restart: unless-stopped

  pebble:
    image: ghcr.io/letsencrypt/pebble:latest
    container_name: certctl-test-pebble
    depends_on:
      - pebble-challtestsrv
    environment:
      PEBBLE_VA_NOSLEEP: 1
      PEBBLE_VA_ALWAYS_VALID: 0
    # ENTRYPOINT is /app (the binary). command: provides only the FLAGS.
    command:
      - -config
      - /test/config/pebble-config.json
      - -dnsserver
      - "10.30.50.3:8053"
      - -strict
    volumes:
      - ./test/pebble-config.json:/test/config/pebble-config.json:ro
    networks:
      certctl-test:
        ipv4_address: 10.30.50.4
    restart: unless-stopped

  # ---------------------------------------------------------------------------
  # step-ca — Private CA (Smallstep)
  # ---------------------------------------------------------------------------
  # Auto-bootstraps on first run: generates root CA + JWK provisioner "admin".
  # Root cert: /home/step/certs/root_ca.crt (inside stepca_data volume)
  # Provisioner key: /home/step/secrets/provisioner_key (encrypted JWK)
  step-ca:
    image: smallstep/step-ca:latest
    container_name: certctl-test-stepca
    environment:
      DOCKER_STEPCA_INIT_NAME: "certctl-test-ca"
      DOCKER_STEPCA_INIT_DNS_NAMES: "step-ca,localhost"
      DOCKER_STEPCA_INIT_PROVISIONER_NAME: "admin"
      DOCKER_STEPCA_INIT_PASSWORD: "password123"
      DOCKER_STEPCA_INIT_ADDRESS: ":9000"
    volumes:
      - stepca_data:/home/step
    networks:
      certctl-test:
        ipv4_address: 10.30.50.5
    healthcheck:
      test: ["CMD", "curl", "-fk", "https://localhost:9000/health"]
      interval: 10s
      timeout: 5s
      start_period: 15s
      retries: 10
    restart: unless-stopped

  # ---------------------------------------------------------------------------
  # certctl Server (Control Plane)
  # ---------------------------------------------------------------------------
  # Connects to PostgreSQL, Pebble (ACME), step-ca, and Local CA.
  #
  # TLS trust problem: Pebble and step-ca use self-signed root CAs that
  # aren't in Alpine's trust store. The ACME and step-ca connectors use
  # Go's default http.Client (no InsecureSkipVerify), so they need the
  # CA certs in the system trust store.
  #
  # Solution: setup-trust.sh runs as root, fetches Pebble CA from its
  # management API, copies step-ca root cert from the shared volume,
  # runs update-ca-certificates, then execs the server binary.
  certctl-server:
    build:
      context: ..
      dockerfile: Dockerfile
      # Proxy propagation (M-4, Issue #9) — forwards host shell's proxy env
      # vars into the Docker build so the Node frontend stage and Go module
      # download can reach the public registries behind corporate proxies.
      # Defaults to empty; omit the variables from the host environment for
      # un-proxied builds and the behaviour is byte-identical to the pre-fix
      # tree.
      args:
        HTTP_PROXY: ${HTTP_PROXY:-}
        HTTPS_PROXY: ${HTTPS_PROXY:-}
        NO_PROXY: ${NO_PROXY:-}
    container_name: certctl-test-server
    depends_on:
      postgres:
        condition: service_healthy
      pebble:
        condition: service_started
      step-ca:
        condition: service_healthy
      # HTTPS-Everywhere Phase 6: block server boot until the init container
      # has written server.crt / server.key / ca.crt into ./test/certs. The
      # init container runs once and exits 0; service_completed_successfully
      # makes that a gating dependency rather than a liveness one.
      certctl-tls-init:
        condition: service_completed_successfully
    # Run as root so update-ca-certificates can write to /etc/ssl/certs.
    # Container isolation provides the security boundary.
    user: "0:0"
    entrypoint: ["/bin/sh", "/app/setup-trust.sh"]
    environment:
      # Database
      CERTCTL_DATABASE_URL: postgres://certctl:testpass@postgres:5432/certctl?sslmode=disable

      # Server
      CERTCTL_SERVER_HOST: 0.0.0.0
      CERTCTL_SERVER_PORT: 8443
      # HTTPS-Everywhere Phase 6: point the server at the init-container-generated
      # cert/key pair (bind-mounted from ./test/certs). Same paths as production
      # compose so the server binary code path is identical; only the host-side
      # storage differs (bind mount vs named volume — see §certctl-tls-init block).
      CERTCTL_SERVER_TLS_CERT_PATH: /etc/certctl/tls/server.crt
      CERTCTL_SERVER_TLS_KEY_PATH: /etc/certctl/tls/server.key
      CERTCTL_LOG_LEVEL: debug

      # Auth — API key required (production-like)
      CERTCTL_AUTH_TYPE: api-key
      CERTCTL_AUTH_SECRET: test-key-2026

      # Key generation — agent-side (production-like)
      CERTCTL_KEYGEN_MODE: agent

      # Local CA issuer (iss-local) — self-signed mode (no CA cert/key paths)
      # This is the simplest issuer, always available.

      # ACME issuer (iss-acme-staging) — pointed at Pebble
      CERTCTL_ACME_DIRECTORY_URL: https://pebble:14000/dir
      CERTCTL_ACME_EMAIL: test@certctl.dev
      CERTCTL_ACME_CHALLENGE_TYPE: http-01
      CERTCTL_ACME_INSECURE: "true"

      # step-ca issuer (iss-stepca)
      CERTCTL_STEPCA_URL: https://step-ca:9000
      CERTCTL_STEPCA_ROOT_CERT: /stepca-data/certs/root_ca.crt
      CERTCTL_STEPCA_PROVISIONER: admin
      CERTCTL_STEPCA_PASSWORD: password123
      CERTCTL_STEPCA_KEY_PATH: /stepca-data/secrets/provisioner_key

      # EST server (RFC 7030) — uses Local CA by default
      CERTCTL_EST_ENABLED: "true"
      CERTCTL_EST_ISSUER_ID: iss-local

      # Dynamic issuer/target config encryption (M34/M35)
      CERTCTL_CONFIG_ENCRYPTION_KEY: test-encryption-key-32chars!!

      # Network scanning
      CERTCTL_NETWORK_SCAN_ENABLED: "true"

      # Post-deployment TLS verification
      CERTCTL_VERIFY_DEPLOYMENT: "true"
      CERTCTL_VERIFY_TIMEOUT: "10s"
      CERTCTL_VERIFY_DELAY: "3s"
    ports:
      - "8443:8443"
    volumes:
      - ./test/setup-trust.sh:/app/setup-trust.sh:ro
      # step-ca data volume (root cert at /certs/root_ca.crt, key at /secrets/provisioner_key)
      - stepca_data:/stepca-data:ro
      # HTTPS-Everywhere Phase 6: read-only bind mount of the init-generated
      # TLS material. The init container writes here; server reads here; the
      # agent mounts the same host path at the same container path (see below)
      # so /etc/certctl/tls/ca.crt resolves to the *same* bytes on both sides.
      - ./test/certs:/etc/certctl/tls:ro
    networks:
      certctl-test:
        ipv4_address: 10.30.50.6
    healthcheck:
      # HTTPS-Everywhere Phase 6: healthcheck now speaks TLS with --cacert to
      # verify the self-signed server cert against the init-generated bundle.
      # /health requires auth when CERTCTL_AUTH_TYPE=api-key, so include the
      # Bearer token. curl exits non-zero on both TLS handshake failure and
      # non-2xx status — either failure keeps depends_on: {condition:
      # service_healthy} from unblocking the agent, which is what we want.
      test: ["CMD", "curl", "--cacert", "/etc/certctl/tls/ca.crt", "-f", "-H", "Authorization: Bearer test-key-2026", "https://localhost:8443/health"]
      interval: 10s
      timeout: 5s
      start_period: 30s
      retries: 10
    restart: unless-stopped

  # ---------------------------------------------------------------------------
  # NGINX — TLS Target Server
  # ---------------------------------------------------------------------------
  # The agent deploys certificates here via the shared nginx_certs volume.
  # nginx-entrypoint.sh generates a self-signed placeholder cert so NGINX
  # can boot before the agent deploys a real cert.
  #
  # Ports: 8080 (HTTP) / 8444 (HTTPS) — offset to avoid conflict with server.
  nginx:
    image: nginx:alpine
    container_name: certctl-test-nginx
    entrypoint: ["/bin/sh", "/entrypoint.sh"]
    volumes:
      - ./test/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./test/nginx-entrypoint.sh:/entrypoint.sh:ro
      - nginx_certs:/etc/nginx/certs
    ports:
      - "8080:80"
      - "8444:443"
    networks:
      certctl-test:
        ipv4_address: 10.30.50.7
    healthcheck:
      test: ["CMD-SHELL", "curl -fk https://localhost/health || exit 1"]
      interval: 10s
      timeout: 5s
      start_period: 15s
      retries: 5
    restart: unless-stopped

  # ---------------------------------------------------------------------------
  # certctl Agent
  # ---------------------------------------------------------------------------
  # Polls the server for work, generates ECDSA P-256 keys locally,
  # deploys certs to NGINX via the shared volume, and discovers existing
  # certs in the NGINX cert directory.
  certctl-agent:
    build:
      context: ..
      dockerfile: Dockerfile.agent
      # Proxy propagation (M-4, Issue #9) — forwards host shell's proxy env
      # vars into the Docker build so the Go module download stage can reach
      # the public Go module proxy behind corporate proxies. Defaults to
      # empty; omit the variables from the host environment for un-proxied
      # builds and the behaviour is byte-identical to the pre-fix tree.
      args:
        HTTP_PROXY: ${HTTP_PROXY:-}
        HTTPS_PROXY: ${HTTPS_PROXY:-}
        NO_PROXY: ${NO_PROXY:-}
    container_name: certctl-test-agent
    depends_on:
      certctl-server:
        condition: service_healthy
    environment:
      # HTTPS-Everywhere Phase 6: agent dials the server over TLS and validates
      # the self-signed cert against the CA bundle pinned by
      # CERTCTL_SERVER_CA_BUNDLE_PATH. Same env vars + container paths as
      # production compose so the agent binary code path (loadCABundle →
      # x509.CertPool → *tls.Config{RootCAs, MinVersion: TLS13}) is identical.
      CERTCTL_SERVER_URL: https://certctl-server:8443
      CERTCTL_SERVER_CA_BUNDLE_PATH: /etc/certctl/tls/ca.crt
      CERTCTL_API_KEY: test-key-2026
      CERTCTL_AGENT_NAME: test-agent-01
      CERTCTL_AGENT_ID: agent-test-01
      CERTCTL_KEYGEN_MODE: agent
      CERTCTL_LOG_LEVEL: debug
      CERTCTL_DISCOVERY_DIRS: /nginx-certs
    volumes:
      - agent_keys:/var/lib/certctl/keys
      - nginx_certs:/nginx-certs
      # HTTPS-Everywhere Phase 6: same bind mount as the server, same path,
      # so /etc/certctl/tls/ca.crt resolves to the identical bytes. This is
      # the only way the CN=certctl-server cert validates on the agent side.
      - ./test/certs:/etc/certctl/tls:ro
    networks:
      certctl-test:
        ipv4_address: 10.30.50.8
    restart: unless-stopped

# =============================================================================
# Network
# =============================================================================
# Static IPs are required because:
# - Pebble needs to know the challtestsrv DNS server address (10.30.50.3)
# - challtestsrv resolves all domains to certctl-server (10.30.50.6) for HTTP-01 challenges
# - Avoids DNS race conditions during startup
networks:
  certctl-test:
    driver: bridge
    ipam:
      config:
        - subnet: 10.30.50.0/24

# =============================================================================
# Volumes
# =============================================================================
volumes:
  test_postgres_data:
    driver: local
  stepca_data:
    driver: local
  agent_keys:
    driver: local
  nginx_certs:
    driver: local
