# docker-dev/ — Mac-friendly single-mesh hub-and-spoke fleet for v2 development + manual UI exercise.
#
# Topology: ONE Akka mesh seeded by `central-1`. Logical separation between
# tenants is by ServerCluster.ClusterId rows (MAIN / SITE-A / SITE-B) in the one
# shared `OtOpcUa` ConfigDb — NOT by separate meshes. All six host nodes join the
# same gossip ring and the central UI deploys to every cluster over it.
#
# Stack:
#   sql           SQL Server 2022 — hosts the one ConfigDb every node uses
#   cluster-seed  one-shot mssql-tools job that INSERTs the ServerCluster +
#                 ClusterNode rows scoping each tenant, then exits (idempotent)
#
#   central-1, central-2   OTOPCUA_ROLES=admin,driver — the ONLY UI + deploy
#                          singleton, plus the MAIN cluster's OPC UA publishers.
#                          Reachable at http://localhost:9200 (via Traefik).
#                          central-1 is the Akka seed node; central-2 joins it.
#   site-a-1, site-a-2     OTOPCUA_ROLES=driver — driver-only members of the same
#   site-b-1, site-b-2     mesh, scoped to SITE-A / SITE-B by ClusterId. They
#                          serve no UI and authenticate no users; the central
#                          cluster manages and deploys to them over the mesh.
#
#   Auth is real LDAP against the shared GLAuth on the Linux Docker host
#   (10.100.0.35:3893, dc=zb,dc=local) — there is no LDAP container here.
#   Only the admin-role central nodes carry the Security__Ldap__* block.
#   Sign in `multi-role` / `password`.
#
#   traefik    PathPrefix(`/`) → central-1 / central-2 (the single UI route).
#
# OPC UA endpoints (host-side port → container 4840):
#   central-1 :4840   central-2 :4841
#   site-a-1  :4842   site-a-2  :4843
#   site-b-1  :4844   site-b-2  :4845
#
# Headless deploy: POST http://localhost:9200/api/deployments with the
# X-Api-Key header (Security__DeployApiKey = "docker-dev-deploy-key").
#
# SQL persistence: the otopcua-mssql-data named volume keeps the ConfigDb schema
# + seeded clusters across `docker compose up` cycles; without it a recreate
# silently drops the OtOpcUa database.
#
# Usage:
#   docker compose -f docker-dev/docker-compose.yml up -d --build
#   open http://localhost:9200             # central Blazor admin UI
#   open http://localhost:8089             # Traefik dashboard (8080 is the sister scadalink stack)
#
# Tear-down:  docker compose -f docker-dev/docker-compose.yml down -v

name: otopcua-dev

services:

  sql:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "OtOpcUa!Dev123"
      MSSQL_PID: Developer
    ports:
      - "14330:1433"
    # Persist the ConfigDb across container recreates. Without this the dev SQL
    # is ephemeral (container writable layer), so a recreate silently drops the
    # OtOpcUa database and every host node fails its configdb health check until
    # EF auto-migration + cluster-seed rebuild it. The named volume keeps the
    # schema + seeded clusters between `docker compose up` cycles.
    volumes:
      - otopcua-mssql-data:/var/opt/mssql
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'OtOpcUa!Dev123' -No -Q 'SELECT 1' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 20

  # ── Migrator (one-shot) ────────────────────────────────────────────────────
  # Applies EF Core migrations to the OtOpcUa ConfigDb so a fresh SQL volume gets
  # the schema with no operator step (the host nodes deliberately don't auto-
  # migrate — production owns schema changes). cluster-seed + every host node
  # depend on this completing, so nothing races an in-progress migration.
  # Idempotent: a no-op once the schema is current.
  migrator:
    build:
      context: ..
      dockerfile: docker-dev/Dockerfile
      target: migrator
    image: otopcua-migrator:dev
    depends_on:
      sql:
        condition: service_healthy
    environment:
      OTOPCUA_CONFIG_CONNECTION: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
    restart: "no"

  # ── Cluster seed (one-shot) ────────────────────────────────────────────────
  # Runs only after `migrator` completes (so the schema is final — no race), then
  # INSERTs the three ServerCluster rows and the six ClusterNode rows that scope
  # each tenant inside the shared OtOpcUa ConfigDb. Idempotent — re-runs are no-ops.
  cluster-seed:
    image: mcr.microsoft.com/mssql-tools:latest
    depends_on:
      migrator:
        condition: service_completed_successfully
    volumes:
      - ./seed:/seed:ro
    entrypoint: ["/bin/bash", "/seed/entrypoint.sh"]
    restart: "no"

  # A local OpenLDAP container used to live here, but the bitnami/openldap:2.6
  # image was retired (manifest gone) and bitnamilegacy/openldap:2.6 crashes
  # during LDIF setup (exit 68). Rather than stub auth, the central (admin-role)
  # containers bind the shared GLAuth on the Linux Docker host (Security__Ldap__*
  # below: 10.100.0.35:3893, dc=zb,dc=local, DevStubMode=false) — so dev auth
  # exercises the real LDAP bind + group→role path. Sign in `multi-role` /
  # `password` (all roles) or any shared test user / `password`.

  # ── Central cluster (2-node fused admin+driver) ─────────────────────────────
  # The only UI + deploy singleton; also the MAIN cluster's OPC UA publishers.
  # central-1 seeds the single Akka mesh that every other node joins.

  central-1: &otopcua-host
    build:
      context: ..
      dockerfile: docker-dev/Dockerfile
      target: runtime
    image: otopcua-host:dev
    # Per-node memory bounds. The full single-mesh stack (6 host nodes) OOM-killed
    # central-1 on a loaded host. Each host node measured ~357 MiB idle-solo and
    # climbs sharply under deploy/materialise load: a node materialising its full
    # cluster slice (e.g. central → MAIN's galaxy mirror + UNS overlay, ~1400 OPC UA
    # nodes) peaks well above 1g during a deploy — a 1g cap OOM-kills it (exit 137).
    # Cap at 2g (≈peak + headroom) with a 1g reservation. These top-level keys are
    # inherited by every service that uses `<<: *otopcua-host` (YAML merge keeps the
    # anchor's scalar keys; only the `environment` block is re-declared per service).
    # The full 6-node mesh needs ~12g of Docker Desktop VM memory — on a constrained
    # host raise the VM memory or run fewer host services.
    mem_limit: 2g
    mem_reservation: 1g
    depends_on:
      sql: { condition: service_healthy }
      migrator: { condition: service_completed_successfully }
    environment:
      OTOPCUA_ROLES: "admin,driver"
      ASPNETCORE_URLS: "http://+:9000"
      ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
      Cluster__Hostname: "0.0.0.0"
      Cluster__Port: "4053"
      Cluster__PublicHostname: "central-1"
      Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053"
      Cluster__Roles__0: "admin"
      Cluster__Roles__1: "driver"
      Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
      Security__Jwt__Issuer: "otopcua-dev"
      Security__Jwt__Audience: "otopcua-dev"
      Security__Ldap__Enabled: "true"
      Security__Ldap__DevStubMode: "false"
      Security__Ldap__Server: "10.100.0.35"
      Security__Ldap__Port: "3893"
      Security__Ldap__Transport: "None"
      Security__Ldap__AllowInsecure: "true"
      Security__Ldap__SearchBase: "dc=zb,dc=local"
      Security__Ldap__ServiceAccountDn: "cn=serviceaccount,dc=zb,dc=local"
      Security__Ldap__ServiceAccountPassword: "serviceaccount123"
      Security__DeployApiKey: "docker-dev-deploy-key"
      Security__Auth__DisableLogin: "true"
      # Pin EF Core + ASP.NET Core to Warning so the per-poll Deployment SELECT /
      # "Executed DbCommand" Information|Debug lines stop flooding the Serilog
      # pipeline and starving the Akka cluster heartbeat thread. The host logs via
      # Serilog (AddZbSerilog → ReadFrom.Configuration); these env vars override
      # Serilog:MinimumLevel:Override:* (app/Akka levels are left untouched).
      Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
      Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
      GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
    ports:
      - "4840:4840"

  central-2:
    <<: *otopcua-host
    depends_on:
      sql: { condition: service_healthy }
      central-1: { condition: service_started }
      migrator: { condition: service_completed_successfully }
    environment:
      OTOPCUA_ROLES: "admin,driver"
      ASPNETCORE_URLS: "http://+:9000"
      ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
      Cluster__Hostname: "0.0.0.0"
      Cluster__Port: "4053"
      Cluster__PublicHostname: "central-2"
      Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053"
      Cluster__Roles__0: "admin"
      Cluster__Roles__1: "driver"
      Security__Jwt__SigningKey: "docker-dev-signing-key-with-at-least-32-bytes-of-utf8-content-12345"
      Security__Jwt__Issuer: "otopcua-dev"
      Security__Jwt__Audience: "otopcua-dev"
      Security__Ldap__Enabled: "true"
      Security__Ldap__DevStubMode: "false"
      Security__Ldap__Server: "10.100.0.35"
      Security__Ldap__Port: "3893"
      Security__Ldap__Transport: "None"
      Security__Ldap__AllowInsecure: "true"
      Security__Ldap__SearchBase: "dc=zb,dc=local"
      Security__Ldap__ServiceAccountDn: "cn=serviceaccount,dc=zb,dc=local"
      Security__Ldap__ServiceAccountPassword: "serviceaccount123"
      Security__DeployApiKey: "docker-dev-deploy-key"
      Security__Auth__DisableLogin: "true"
      # Quiet EF/AspNetCore SQL flood — see central-1 (Serilog override). mem_limit/
      # mem_reservation are inherited from the *otopcua-host anchor.
      Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
      Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
      GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
    ports:
      - "4841:4840"

  # ── Site A cluster (2-node driver-only) ─────────────────────────────────────
  # Driver-only members of the single mesh, scoped to SITE-A by ClusterId. No UI,
  # no user auth — managed + deployed to by the central cluster over the mesh.
  # All site nodes seed central-1.

  site-a-1:
    <<: *otopcua-host
    depends_on:
      sql: { condition: service_healthy }
      central-1: { condition: service_started }
      migrator: { condition: service_completed_successfully }
    environment:
      OTOPCUA_ROLES: "driver"
      ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
      Cluster__Hostname: "0.0.0.0"
      Cluster__Port: "4053"
      Cluster__PublicHostname: "site-a-1"
      Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053"
      Cluster__Roles__0: "driver"
      # Quiet EF/AspNetCore SQL flood — see central-1 (Serilog override). mem_limit/
      # mem_reservation are inherited from the *otopcua-host anchor.
      Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
      Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
      # Resolved at runtime by GalaxyDriver.ResolveApiKey when a DriverInstance's
      # Gateway.ApiKeySecretRef = "env:GALAXY_MXGW_API_KEY".
      GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
    ports:
      - "4842:4840"

  site-a-2:
    <<: *otopcua-host
    depends_on:
      sql: { condition: service_healthy }
      central-1: { condition: service_started }
      migrator: { condition: service_completed_successfully }
    environment:
      OTOPCUA_ROLES: "driver"
      ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
      Cluster__Hostname: "0.0.0.0"
      Cluster__Port: "4053"
      Cluster__PublicHostname: "site-a-2"
      Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053"
      Cluster__Roles__0: "driver"
      Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
      Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
      GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
    ports:
      - "4843:4840"

  # ── Site B cluster (2-node driver-only) ─────────────────────────────────────

  site-b-1:
    <<: *otopcua-host
    depends_on:
      sql: { condition: service_healthy }
      central-1: { condition: service_started }
      migrator: { condition: service_completed_successfully }
    environment:
      OTOPCUA_ROLES: "driver"
      ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
      Cluster__Hostname: "0.0.0.0"
      Cluster__Port: "4053"
      Cluster__PublicHostname: "site-b-1"
      Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053"
      Cluster__Roles__0: "driver"
      Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
      Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
      GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
    ports:
      - "4844:4840"

  site-b-2:
    <<: *otopcua-host
    depends_on:
      sql: { condition: service_healthy }
      central-1: { condition: service_started }
      migrator: { condition: service_completed_successfully }
    environment:
      OTOPCUA_ROLES: "driver"
      ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;"
      Cluster__Hostname: "0.0.0.0"
      Cluster__Port: "4053"
      Cluster__PublicHostname: "site-b-2"
      Cluster__SeedNodes__0: "akka.tcp://otopcua@central-1:4053"
      Cluster__Roles__0: "driver"
      Serilog__MinimumLevel__Override__Microsoft.EntityFrameworkCore: "Warning"
      Serilog__MinimumLevel__Override__Microsoft.AspNetCore: "Warning"
      GALAXY_MXGW_API_KEY: "${GALAXY_MXGW_API_KEY:-mxgw_otopcua2_GI7-tNozYE6cXGUSgEzL3AHDV7bYcYIHdMwKYgyHdX4}"
    ports:
      - "4845:4840"

  traefik:
    image: traefik:v3.1
    command:
      - --entrypoints.web.address=:80
      - --providers.file.filename=/etc/traefik/dynamic.yml
      - --providers.file.watch=true
      - --api.insecure=true
    ports:
      - "9200:80"  # host port 9200 → traefik :80 entrypoint (80 conflicts with scadabridge-traefik)
      - "8089:8080"  # 8080 conflicts with the sister scadalink dev stack
    volumes:
      - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
    depends_on:
      - central-1
      - central-2

volumes:
  # SQL Server data dir — persists the OtOpcUa ConfigDb across container recreates.
  otopcua-mssql-data:
