# FreeIPA + Keycloak + PostgreSQL constellation
#
# Setup:
#   cp .env.example .env && $EDITOR .env
#   docker compose up -d
#   docker compose logs -f freeipa       # watch first-boot install (~10 min)
#   # Once freeipa is healthy:
#   ./keycloak-configure.sh              # wire Keycloak → FreeIPA LDAP
#
# To run without Keycloak:
#   docker compose up -d freeipa
#
# To scale out (clients enroll against the same IPA server):
#   docker compose --profile clients up
#
# ── cgroupns NOTE ────────────────────────────────────────────────────────────
# On hosts using cgroup v2 (WSL2, recent Linux with unified hierarchy) the
# FreeIPA container runs /sbin/init (systemd) and exits immediately with code
# 255 unless the container shares the host cgroup namespace.  Docker Compose
# does not yet expose a cgroupns_mode property in its schema, so use the
# provided run.sh wrapper instead of plain `docker compose up` on those hosts:
#
#   ./run.sh           — starts freeipa only (cgroupns host applied automatically)
#   ./run.sh all       — starts freeipa + postgres + keycloak
#
# On plain cgroup v1 hosts `docker compose up -d` works without run.sh.

# Volume names are pinned (name:) so they match what run.sh creates with plain
# `docker volume create` on cgroup-v2 hosts. Without this, compose prefixes the
# project name (image_cmk-creds, ...) and the freeipa container started by
# run.sh mounts a DIFFERENT cmk-creds volume than the compose-managed checkmk
# container writes to — the automation secret then never reaches IPA and the
# CheckMK integration retries forever.
volumes:
  freeipa-data:
    name: freeipa-data
  keycloak-db:
    name: keycloak-db
  cmk-data:    # CheckMK site data (persistent across restarts)
    name: cmk-data
  cmk-creds:   # shared: CheckMK writes automation.secret here; FreeIPA reads it
    name: cmk-creds

networks:
  ipa-net:
    # Pin the real network name: run.sh (cgroup-v2 hosts) creates/uses a bare
    # "ipa-net", while compose would otherwise create a project-prefixed
    # "image_ipa-net" on the same subnet — the second one to run then fails
    # with "Pool overlaps with other one on this address space".
    name: ipa-net
    ipam:
      config:
        - subnet: 172.30.0.0/24

services:

  # ── FreeIPA ─────────────────────────────────────────────────────────────────
  freeipa:
    build:
      context: .
      dockerfile: Dockerfile
    image: freeipa-server:local
    container_name: freeipa
    hostname: ${IPA_HOSTNAME:-ipa.example.com}
    privileged: true
    tmpfs:
      - /run
      - /tmp
    volumes:
      - freeipa-data:/data
      - /sys/fs/cgroup:/sys/fs/cgroup:rw
      - cmk-creds:/cmk-creds       # read CMK secret + write push-mode requests for hosts
    environment:
      IPA_DOMAIN:             ${IPA_DOMAIN:?set IPA_DOMAIN in .env}
      IPA_REALM:              ${IPA_REALM:-}
      IPA_ADMIN_PASSWORD:     ${IPA_ADMIN_PASSWORD:?set IPA_ADMIN_PASSWORD in .env}
      IPA_DM_PASSWORD:        ${IPA_DM_PASSWORD:?set IPA_DM_PASSWORD in .env}
      IPA_SETUP_DNS:          ${IPA_SETUP_DNS:-false}
      IPA_DNS_FORWARDER:      ${IPA_DNS_FORWARDER:-}
      IPA_SETUP_KRA:          ${IPA_SETUP_KRA:-false}
      CMK_URL:                http://172.30.0.12:5000
      # Client-facing CheckMK URL stored in the dev_mon_base description —
      # must be routable from enrolled clients (e.g. http://<docker-host>:8090).
      CMK_ADVERTISED_URL:     ${CMK_ADVERTISED_URL:-}
      CMK_SITE_ID:            ${CMK_SITE_ID:-cmk}
    ports:
      - "389:389"
      - "636:636"
      - "88:88"
      - "88:88/udp"
      - "464:464"
      - "464:464/udp"
      - "443:443"
      - "445:445"
      - "139:139"
      - "137:137/udp"
      - "138:138/udp"
    networks:
      ipa-net:
        ipv4_address: 172.30.0.10
    healthcheck:
      test: ["CMD-SHELL", "ipactl status 2>/dev/null | grep -q 'running'"]
      interval: 30s
      timeout: 15s
      retries: 20
      start_period: 600s

  # ── PostgreSQL (Keycloak backend) ────────────────────────────────────────────
  postgres:
    image: postgres:16-alpine
    container_name: keycloak-db
    restart: unless-stopped
    environment:
      POSTGRES_DB:       keycloak
      POSTGRES_USER:     keycloak
      POSTGRES_PASSWORD: ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
    volumes:
      - keycloak-db:/var/lib/postgresql/data
    networks:
      ipa-net:
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "keycloak"]
      interval: 10s
      retries: 5

  # ── CheckMK CE ───────────────────────────────────────────────────────────────
  # Monitoring server.  Clients register themselves via ansipa-enforce-policies.sh
  # when added to dev_mon_base.  The automation user's secret is written to the
  # cmk-creds shared volume so ansipa-checkmk-setup.sh (in the freeipa container)
  # can store it in the IPA dev_mon_base hostgroup description for client discovery.
  #
  # Web UI:   http://localhost:8090/cmk/   (user: cmkadmin, pass: CMK_ADMIN_PASSWORD)
  # Agent dl: http://localhost:8090/cmk/check_mk/agents/
  # Note: CheckMK 2.3 uses port 5000 (OMD apache); host port 8090 maps to it.
  checkmk:
    image: checkmk/check-mk-raw:2.3.0-latest
    container_name: checkmk
    hostname: checkmk.test.local
    restart: unless-stopped
    # Override entrypoint to copy the auto-generated automation secret to the
    # shared volume so the freeipa container can read it without docker exec.
    # $$ → $ after Compose expansion (i.e., $$! → $! in bash = background PID).
    command:
      - bash
      - -c
      - >-
        /docker-entrypoint.sh & CMK_PID=$$!;
        until [ -f /omd/sites/${CMK_SITE_ID:-cmk}/var/check_mk/web/automation/automation.secret ];
        do sleep 3; done; sleep 5;
        mkdir -p /cmk-creds;
        cp /omd/sites/${CMK_SITE_ID:-cmk}/var/check_mk/web/automation/automation.secret /cmk-creds/automation.secret;
        chmod 644 /cmk-creds/automation.secret;
        printf '<LocationMatch ^/cmk/(omd|wiki|nagvis|check_mk|pnp4nagios|api)>\n    Order allow,deny\n    Allow from all\n    Satisfy any\n</LocationMatch>\n'
        > /omd/sites/${CMK_SITE_ID:-cmk}/etc/apache/conf.d/cookie_auth.conf;
        omd reload ${CMK_SITE_ID:-cmk} apache 2>/dev/null || true;
        while true; do
            for _req in /cmk-creds/push-mode-*.req; do
                [ -f "$$_req" ] || continue;
                _hn=$$(cat "$$_req");
                _hk=$$(echo "$$_hn" | tr . _);
                _mk=/omd/sites/${CMK_SITE_ID:-cmk}/etc/check_mk/conf.d/wato/ansipa/hosts.mk;
                [ -f "$$_mk" ] || continue;
                grep -q "explicit_host_conf.*$$_hk\|'$$_hn'.*push-agent" "$$_mk" 2>/dev/null && { rm -f "$$_req"; continue; };
                printf '\nexplicit_host_conf.setdefault('"'"'cmk_agent_connection'"'"', {})\nexplicit_host_conf['"'"'cmk_agent_connection'"'"'].update({'"'"'%s'"'"': '"'"'push-agent'"'"'})\n' "$$_hn" >> "$$_mk";
                su - ${CMK_SITE_ID:-cmk} -c 'cmk -O' 2>/dev/null || true;
                rm -f "$$_req";
            done;
            sleep 30;
        done &
        wait $$CMK_PID
    environment:
      CMK_SITE_ID:  ${CMK_SITE_ID:-cmk}
      CMK_PASSWORD: ${CMK_ADMIN_PASSWORD:?set CMK_ADMIN_PASSWORD in .env}
    ports:
      - "8090:5000"   # web UI (CheckMK 2.3 uses port 5000 for OMD apache)
      - "8000:8000"   # agent receiver (push mode / cmk-agent-ctl)
      - "6557:6557"   # livestatus (optional, for external tools)
    volumes:
      - cmk-data:/omd/sites
      - cmk-creds:/cmk-creds
    networks:
      ipa-net:
        ipv4_address: 172.30.0.12
    healthcheck:
      # Probe the login page, not /api/1.0/version: the REST API version
      # endpoint requires authentication in CheckMK 2.3, so an unauthenticated
      # curl -f gets 401 and the container stays "unhealthy" forever even
      # though the site is fully up.
      test: ["CMD-SHELL", "curl -sf http://localhost:5000/${CMK_SITE_ID:-cmk}/check_mk/login.py -o /dev/null"]
      interval: 30s
      timeout: 15s
      retries: 20
      start_period: 180s

  # ── Keycloak ─────────────────────────────────────────────────────────────────
  # After first start, run ./keycloak-configure.sh to wire FreeIPA LDAP federation.
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    container_name: keycloak
    restart: unless-stopped
    # Use 'start' (not start-dev) for production; requires a TLS certificate.
    command: start-dev
    environment:
      KC_DB:                   postgres
      KC_DB_URL:               jdbc:postgresql://postgres/keycloak
      KC_DB_USERNAME:          keycloak
      KC_DB_PASSWORD:          ${KC_DB_PASSWORD:?set KC_DB_PASSWORD in .env}
      KC_HOSTNAME:             ${KC_HOSTNAME:-localhost}
      KC_HTTP_PORT:            8080
      KC_HTTPS_PORT:           8443
      KC_HTTP_ENABLED:         "true"
      KC_HEALTH_ENABLED:       "true"   # exposes /health/* on management port 9000 (KC 25+)
      # Serve Keycloak under /auth so the ansipa nginx gateway can proxy it at a
      # native subpath (no rewriting). KC_PROXY_HEADERS=xforwarded makes Keycloak
      # trust X-Forwarded-* so it builds correct absolute URLs behind the gateway
      # (and behind a further upstream proxy). KC_HOSTNAME_STRICT=false keeps it
      # from pinning to a single hostname during testing.
      KC_HTTP_RELATIVE_PATH:   /auth
      KC_PROXY_HEADERS:        xforwarded
      KC_HOSTNAME_STRICT:      "false"
      KC_FEATURES:             preview
      KEYCLOAK_ADMIN:          ${KC_ADMIN:-admin}
      KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:?set KC_ADMIN_PASSWORD in .env}
    extra_hosts:
      - "${IPA_HOSTNAME:-ipa.example.com}:172.30.0.10"
    ports:
      - "8080:8080"
      - "8443:8443"
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      ipa-net:
    healthcheck:
      # The official Keycloak image ships neither curl nor wget (but has bash),
      # so probe with bash's /dev/tcp. Check the actual serving port under the
      # configured relative path (/auth) rather than the 9000 management
      # /health endpoint — with KC_HTTP_RELATIVE_PATH set, /health/ready on 9000
      # returns 404, whereas /auth/realms/master reliably returns 200 once up.
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET /auth/realms/master HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && head -c 15 <&3 | grep -q '200 OK'"]
      interval: 20s
      timeout: 10s
      retries: 20
      start_period: 90s

  # ── nginx gateway: reverse proxy + portal start-page ─────────────────────────
  # Single HTTP entry point that fronts all three web UIs on one hostname:
  #   /            → portal start page (links to each service)
  #   /ipa/        → FreeIPA web UI      (proxied to the freeipa container, 443)
  #   /cmk/        → CheckMK             (proxied to the checkmk container, 5000)
  #   /auth/       → Keycloak            (KC served under /auth relative path)
  #
  # It speaks plain HTTP and trusts X-Forwarded-* so it is meant to sit BEHIND
  # the operator's own TLS-terminating reverse proxy (see .env ANSIPA_* vars and
  # the README snippet for the one-line upstream config). Published on
  # ANSIPA_HTTP_PORT for direct access/testing when no upstream proxy is used.
  nginx:
    image: nginx:alpine
    container_name: ansipa-nginx
    restart: unless-stopped
    environment:
      # Upstream container addresses on ipa-net (envsubst'd into the template).
      ANSIPA_IPA_UPSTREAM: 172.30.0.10
      ANSIPA_CMK_UPSTREAM: 172.30.0.12:5000
      ANSIPA_KC_UPSTREAM:  keycloak:8080
      IPA_HOSTNAME:        ${IPA_HOSTNAME:-ipa.example.com}
    volumes:
      - ./nginx/templates:/etc/nginx/templates:ro   # *.template → envsubst → conf.d
      - ./nginx/portal:/usr/share/nginx/portal:ro
    ports:
      - "${ANSIPA_HTTP_PORT:-8088}:80"
    depends_on:
      - freeipa
      - checkmk
      - keycloak
    networks:
      ipa-net:
    healthcheck:
      # Use 127.0.0.1, not localhost: inside the container localhost resolves to
      # IPv6 ::1, but nginx listens on IPv4 0.0.0.0:80 only, so a localhost probe
      # is refused even though the gateway is serving fine.
      test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 20s
