diff --git a/docker-dev/README.md b/docker-dev/README.md index a9725c0..e393131 100644 --- a/docker-dev/README.md +++ b/docker-dev/README.md @@ -1,6 +1,6 @@ # docker-dev -Mac-friendly multi-cluster OtOpcUa fleet for manual UI exercise + integration smoke tests. Spins up **three isolated Akka clusters** + SQL Server + OpenLDAP + Traefik on the same Compose network. Each cluster has its own ConfigDb database and its own seed-node list, so Akka.Cluster gossip doesn't cross between them even though they share the same system name `otopcua`. +Mac-friendly multi-cluster OtOpcUa fleet for manual UI exercise + integration smoke tests. Spins up **three isolated Akka clusters** + SQL Server + OpenLDAP + Traefik on the same Compose network. All three clusters share the single `OtOpcUa` ConfigDb — multi-tenancy is enforced by per-row `ServerCluster.ClusterId` scoping. Akka.Cluster gossip stays isolated between meshes because their seed-node lists are disjoint, even though they share the same system name `otopcua`. ## Stack @@ -8,11 +8,11 @@ Mac-friendly multi-cluster OtOpcUa fleet for manual UI exercise + integration sm | Service | Role | Ports | |---|---|---| -| `sql` | SQL Server 2022 (hosts all per-cluster ConfigDb databases) | host `14330` → container `1433` | +| `sql` | SQL Server 2022 — single `OtOpcUa` ConfigDb shared by all three clusters | host `14330` → container `1433` | | `ldap` | OpenLDAP with dev users `alice` / `bob` | host `3893` → container `1389` | | `traefik` | Routes :80 by Host header / PathPrefix | host `80`, dashboard `8080` | -### Main cluster — split admin/driver roles (ConfigDb: `OtOpcUa`) +### Main cluster — split admin/driver roles | Service | Role | Ports | |---|---|---| @@ -21,21 +21,33 @@ Mac-friendly multi-cluster OtOpcUa fleet for manual UI exercise + integration sm | `driver-a` | `OTOPCUA_ROLES=driver` | host `4840` → container `4840` | | `driver-b` | `OTOPCUA_ROLES=driver` | host `4841` → container `4840` | -### Site A cluster — 2-node fused admin+driver (ConfigDb: `OtOpcUa_SiteA`) +### Site A cluster — 2-node fused admin+driver | Service | Role | Ports | |---|---|---| | `site-a-1` | `OTOPCUA_ROLES=admin,driver`, cluster seed | host `4842` → container `4840` | | `site-a-2` | `OTOPCUA_ROLES=admin,driver`, joins site-a-1 | host `4843` → container `4840` | -### Site B cluster — 2-node fused admin+driver (ConfigDb: `OtOpcUa_SiteB`) +### Site B cluster — 2-node fused admin+driver | Service | Role | Ports | |---|---|---| | `site-b-1` | `OTOPCUA_ROLES=admin,driver`, cluster seed | host `4844` → container `4840` | | `site-b-2` | `OTOPCUA_ROLES=admin,driver`, joins site-b-1 | host `4845` → container `4840` | -All containers bind Akka remoting to port `4053` inside their own network namespace; the `PublicHostname` of each matches its Compose service name. Cluster isolation is enforced purely by disjoint seed lists. +All containers bind Akka remoting to port `4053` inside their own network namespace; the `PublicHostname` of each matches its Compose service name. Akka mesh isolation is enforced purely by disjoint seed lists. Configuration-side isolation is enforced by `ServerCluster.ClusterId` — see "Multi-tenancy" below. + +## Multi-tenancy + +All eight host nodes write to the same `OtOpcUa` ConfigDb. The `ServerCluster` table differentiates the three Akka meshes: each Akka cluster maps to one row, and each `ClusterNode` row's `ClusterId` ties the runtime node back to its owning cluster scope. After the stack comes up clean for the first time, sign in to any admin UI and create the three rows (or do it via `dotnet run` against the Configuration project's seed script): + +| Akka mesh | Suggested `ClusterId` | Nodes (`ClusterNode.NodeId`) | +|---|---|---| +| Main | `MAIN` | `admin-a`, `admin-b`, `driver-a`, `driver-b` | +| Site A | `SITE-A` | `site-a-1`, `site-a-2` | +| Site B | `SITE-B` | `site-b-1`, `site-b-2` | + +The `NodeId` for each `ClusterNode` row must match the node's `Cluster__PublicHostname` env value (Compose service name) — that's the lookup the runtime uses to find its own membership. ## Bring up diff --git a/docker-dev/docker-compose.yml b/docker-dev/docker-compose.yml index 0a153ed..949117b 100644 --- a/docker-dev/docker-compose.yml +++ b/docker-dev/docker-compose.yml @@ -1,7 +1,7 @@ # docker-dev/ — Mac-friendly multi-cluster fleet for v2 development + manual UI exercise. # -# Stack (3 separate Akka clusters sharing the same SQL + LDAP): -# sql SQL Server 2022 (per-cluster ConfigDb databases) +# Stack (3 separate Akka clusters — all share the single `OtOpcUa` ConfigDb): +# sql SQL Server 2022 — hosts the one ConfigDb that all three clusters use # ldap OpenLDAP with the dev users from C:\publish\glauth\auth.md mirrored in # # Main cluster (existing — split-role admin / driver pair on a single Akka mesh): @@ -9,22 +9,26 @@ # admin-b OtOpcUa.Host with OTOPCUA_ROLES=admin (joins admin-a) # driver-a OtOpcUa.Host with OTOPCUA_ROLES=driver (joins via admin-a) # driver-b OtOpcUa.Host with OTOPCUA_ROLES=driver (joins via admin-a) -# ConfigDb: OtOpcUa # -# Site A cluster (2-node fused admin+driver — its own ConfigDb + seed): +# Site A cluster (2-node fused admin+driver): # site-a-1, site-a-2 OTOPCUA_ROLES=admin,driver, seed = site-a-1 -# ConfigDb: OtOpcUa_SiteA # -# Site B cluster (2-node fused admin+driver — its own ConfigDb + seed): +# Site B cluster (2-node fused admin+driver): # site-b-1, site-b-2 OTOPCUA_ROLES=admin,driver, seed = site-b-1 -# ConfigDb: OtOpcUa_SiteB # # traefik PathPrefix → main cluster admin-a/admin-b; Host(`site-a.localhost`) → # site-a-*; Host(`site-b.localhost`) → site-b-*. Add the two site hosts to # your /etc/hosts (or rely on macOS `.localhost` auto-resolution). # -# Cluster isolation is enforced by disjoint seed-node lists — Akka.Cluster gossip won't -# cross between the three meshes even though they share the same system name "otopcua". +# Multi-tenancy: ConfigDb is one schema with a `ServerCluster` table; each Akka cluster +# corresponds to a row in it (ClusterId = "MAIN" / "SITE-A" / "SITE-B"), and each node's +# `ClusterNode.NodeId` points back at the row that owns it. After first boot, sign in to +# any cluster's Admin UI and create the matching ServerCluster + ClusterNode rows via +# /clusters and /hosts so the runtime knows what configuration scope applies. +# +# Akka mesh isolation: same system name "otopcua" + same remoting port 4053 inside each +# container's own network namespace, but with disjoint seed-node lists — gossip never +# crosses between the three meshes. # # Usage: # docker compose -f docker-dev/docker-compose.yml up -d --build @@ -133,15 +137,16 @@ services: - "4841:4840" # ── Site A cluster (2-node fused admin+driver) ────────────────────────────── - # Its own ConfigDb (OtOpcUa_SiteA) + its own seed (site-a-1) → Akka isolation - # from the main cluster and from site B. Both nodes carry both roles. + # Shares the OtOpcUa ConfigDb with the main + site-b clusters; multi-tenancy is + # enforced by ServerCluster.ClusterId rows (configure via /clusters after boot). + # Akka isolation comes from the disjoint seed list (seed = site-a-1). site-a-1: <<: *otopcua-host environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" - ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa_SiteA;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" + 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" @@ -165,7 +170,7 @@ services: environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" - ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa_SiteA;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" + 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" @@ -188,7 +193,7 @@ services: environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" - ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa_SiteB;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" + 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" @@ -212,7 +217,7 @@ services: environment: OTOPCUA_ROLES: "admin,driver" ASPNETCORE_URLS: "http://+:9000" - ConnectionStrings__ConfigDb: "Server=sql,1433;Database=OtOpcUa_SiteB;User Id=sa;Password=OtOpcUa!Dev123;TrustServerCertificate=True;" + 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"