Some checks failed
v2-ci / build (push) Failing after 32s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Two fixes surfaced while bringing up the docker-dev stack end-to-end:
- HealthEndpoints.MapOtOpcUaHealth now calls .AllowAnonymous() on /health/ready,
/health/active, /healthz. Without it the AddOtOpcUaAuth fallback policy 401s
every probe and Traefik marks every backend unhealthy → all three cluster
routes return 503.
- cluster-seed entrypoint no longer attempts to apply Migrate-To-V2.sql via
sqlcmd. The EF-generated idempotent script puts CREATE PROCEDURE inside
IF NOT EXISTS BEGIN ... END blocks (procs must be first in their batch),
so sqlcmd fails with "Must declare the scalar variable @FromGenerationId".
EF's own runner handles this; sqlcmd doesn't. The seed now just waits for
the schema and applies row inserts. Migrations remain the operator's job:
dotnet ef database update --project src/Core/.../Configuration \
--startup-project src/Server/.../Host
Also: LDAP service removed (bitnami/openldap:2.6 image retired, legacy tag
crashes mid-setup with exit 68); every host now runs with
Authentication__Ldap__DevStubMode=true. Bumped LDAP+Traefik dashboard host
ports to avoid collisions with the sister scadalink dev stack (3893→3894,
8080→8089).
Confirmed working end-to-end: all three Traefik routes return HTTP 200,
cluster-seed populates ServerCluster (MAIN/SITE-A/SITE-B) + ClusterNode
(driver-a/b, site-a-1/2, site-b-1/2) rows on first boot.
252 lines
10 KiB
YAML
252 lines
10 KiB
YAML
# docker-dev/ — Mac-friendly multi-cluster fleet for v2 development + manual UI exercise.
|
|
#
|
|
# 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):
|
|
# admin-a OtOpcUa.Host with OTOPCUA_ROLES=admin (seed)
|
|
# 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)
|
|
#
|
|
# Site A cluster (2-node fused admin+driver):
|
|
# site-a-1, site-a-2 OTOPCUA_ROLES=admin,driver, seed = site-a-1
|
|
#
|
|
# Site B cluster (2-node fused admin+driver):
|
|
# site-b-1, site-b-2 OTOPCUA_ROLES=admin,driver, seed = site-b-1
|
|
#
|
|
# 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).
|
|
#
|
|
# 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
|
|
# open http://localhost # main cluster Blazor admin UI
|
|
# open http://site-a.localhost # site A admin UI
|
|
# open http://site-b.localhost # site B 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"
|
|
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
|
|
|
|
# ── Cluster seed (one-shot) ────────────────────────────────────────────────
|
|
# Waits for SQL + the host containers' EF auto-migration, then INSERTs the
|
|
# three ServerCluster rows and the six ClusterNode rows that scope each Akka
|
|
# mesh inside the shared OtOpcUa ConfigDb. Idempotent — re-runs are no-ops.
|
|
cluster-seed:
|
|
image: mcr.microsoft.com/mssql-tools:latest
|
|
depends_on:
|
|
sql:
|
|
condition: service_healthy
|
|
volumes:
|
|
- ./seed:/seed:ro
|
|
entrypoint: ["/bin/bash", "/seed/entrypoint.sh"]
|
|
restart: "no"
|
|
|
|
# OpenLDAP was previously here but the bitnami/openldap:2.6 image was retired
|
|
# (manifest gone) and bitnamilegacy/openldap:2.6 crashes during LDIF setup with
|
|
# exit 68. For the dev compose every host container now runs with
|
|
# Authentication__Ldap__DevStubMode=true, so any non-empty username/password
|
|
# signs in as `FleetAdmin`. Restore a real LDAP service when there's a need
|
|
# for end-to-end LDAP coverage (the host code path is unchanged).
|
|
|
|
admin-a: &otopcua-host
|
|
build:
|
|
context: ..
|
|
dockerfile: docker-dev/Dockerfile
|
|
image: otopcua-host:dev
|
|
depends_on:
|
|
sql: { condition: service_healthy }
|
|
environment:
|
|
OTOPCUA_ROLES: "admin"
|
|
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: "admin-a"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053"
|
|
Cluster__Roles__0: "admin"
|
|
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"
|
|
Authentication__Ldap__DevStubMode: "true"
|
|
|
|
admin-b:
|
|
<<: *otopcua-host
|
|
environment:
|
|
OTOPCUA_ROLES: "admin"
|
|
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: "admin-b"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053"
|
|
Cluster__Roles__0: "admin"
|
|
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"
|
|
Authentication__Ldap__DevStubMode: "true"
|
|
|
|
driver-a:
|
|
<<: *otopcua-host
|
|
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: "driver-a"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053"
|
|
Cluster__Roles__0: "driver"
|
|
ports:
|
|
- "4840:4840"
|
|
|
|
driver-b:
|
|
<<: *otopcua-host
|
|
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: "driver-b"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@admin-a:4053"
|
|
Cluster__Roles__0: "driver"
|
|
ports:
|
|
- "4841:4840"
|
|
|
|
# ── Site A cluster (2-node fused admin+driver) ──────────────────────────────
|
|
# 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;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@site-a-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"
|
|
Authentication__Ldap__DevStubMode: "true"
|
|
ports:
|
|
- "4842:4840"
|
|
|
|
site-a-2:
|
|
<<: *otopcua-host
|
|
depends_on:
|
|
sql: { condition: service_healthy }
|
|
site-a-1: { condition: service_started }
|
|
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: "site-a-2"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@site-a-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"
|
|
Authentication__Ldap__DevStubMode: "true"
|
|
ports:
|
|
- "4843:4840"
|
|
|
|
# ── Site B cluster (2-node fused admin+driver) ──────────────────────────────
|
|
|
|
site-b-1:
|
|
<<: *otopcua-host
|
|
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: "site-b-1"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@site-b-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"
|
|
Authentication__Ldap__DevStubMode: "true"
|
|
ports:
|
|
- "4844:4840"
|
|
|
|
site-b-2:
|
|
<<: *otopcua-host
|
|
depends_on:
|
|
sql: { condition: service_healthy }
|
|
site-b-1: { condition: service_started }
|
|
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: "site-b-2"
|
|
Cluster__SeedNodes__0: "akka.tcp://otopcua@site-b-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"
|
|
Authentication__Ldap__DevStubMode: "true"
|
|
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:
|
|
- "80:80"
|
|
- "8089:8080" # 8080 conflicts with the sister scadalink dev stack
|
|
volumes:
|
|
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
|
|
depends_on:
|
|
- admin-a
|
|
- admin-b
|
|
- site-a-1
|
|
- site-a-2
|
|
- site-b-1
|
|
- site-b-2
|