Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f02071c9a2 | |||
| 993e012e55 | |||
| 961e09430a | |||
| a1a7646b33 |
+62
-12
@@ -1,20 +1,63 @@
|
|||||||
# docker-dev
|
# docker-dev
|
||||||
|
|
||||||
Mac-friendly four-node OtOpcUa fleet for manual UI exercise + integration smoke tests. Spins up an Akka cluster + SQL Server + OpenLDAP + Traefik in front of two admin nodes.
|
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
|
## Stack
|
||||||
|
|
||||||
|
### Shared infrastructure
|
||||||
|
|
||||||
| Service | Role | Ports |
|
| Service | Role | Ports |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `sql` | SQL Server 2022 (`ConfigDb` backing store) | 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` |
|
| `ldap` | OpenLDAP with dev users `alice` / `bob` | host `3893` → container `1389` |
|
||||||
| `admin-a` | OtOpcUa.Host, `OTOPCUA_ROLES=admin`, cluster seed | internal `9000` |
|
| `traefik` | Routes :80 by Host header / PathPrefix | host `80`, dashboard `8080` |
|
||||||
| `admin-b` | OtOpcUa.Host, `OTOPCUA_ROLES=admin`, joins admin-a | internal `9000` |
|
|
||||||
| `driver-a` | OtOpcUa.Host, `OTOPCUA_ROLES=driver` | host `4840` → container `4840` |
|
|
||||||
| `driver-b` | OtOpcUa.Host, `OTOPCUA_ROLES=driver` | host `4841` → container `4840` |
|
|
||||||
| `traefik` | Routes `:80` to whichever admin-* currently passes `/health/active` | host `80`, dashboard `8080` |
|
|
||||||
|
|
||||||
All six containers share an Akka cluster bound to port `4053` inside the Compose network. The Akka `PublicHostname` of each container matches its Compose service name; the seed-node list points at `admin-a` so the other three join via that.
|
### Main cluster — split admin/driver roles
|
||||||
|
|
||||||
|
| Service | Role | Ports |
|
||||||
|
|---|---|---|
|
||||||
|
| `admin-a` | `OTOPCUA_ROLES=admin`, cluster seed | internal `9000` |
|
||||||
|
| `admin-b` | `OTOPCUA_ROLES=admin`, joins admin-a | internal `9000` |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
| 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
|
||||||
|
|
||||||
|
| 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. 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.
|
||||||
|
|
||||||
|
A one-shot `cluster-seed` Compose service (image `mcr.microsoft.com/mssql-tools`) waits for SQL + the EF auto-migration to complete and then INSERTs the rows below. The seed is **idempotent** — `IF NOT EXISTS` guards every insert — so re-runs on `docker compose up` are no-ops:
|
||||||
|
|
||||||
|
| Akka mesh | `ServerCluster.ClusterId` | `ClusterNode.NodeId` rows |
|
||||||
|
|---|---|---|
|
||||||
|
| Main | `MAIN` | `driver-a`, `driver-b` (OPC UA publishers) |
|
||||||
|
| Site A | `SITE-A` | `site-a-1`, `site-a-2` |
|
||||||
|
| Site B | `SITE-B` | `site-b-1`, `site-b-2` |
|
||||||
|
|
||||||
|
`ClusterNode` is the table for **OPC UA-publishing nodes** (not every Akka cluster member), which is why the main cluster's `admin-a` / `admin-b` don't get rows — they're control-plane-only.
|
||||||
|
|
||||||
|
Each `ClusterNode.NodeId` matches the node's `Cluster__PublicHostname` env value (Compose service name) — that's the lookup the runtime uses to resolve its own membership. `ApplicationUri` follows the `urn:OtOpcUa:<NodeId>` convention.
|
||||||
|
|
||||||
|
The SQL lives at `seed/seed-clusters.sql`; the wait-and-apply wrapper lives at `seed/entrypoint.sh`. To re-seed manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-dev/docker-compose.yml run --rm cluster-seed
|
||||||
|
```
|
||||||
|
|
||||||
## Bring up
|
## Bring up
|
||||||
|
|
||||||
@@ -22,12 +65,16 @@ All six containers share an Akka cluster bound to port `4053` inside the Compose
|
|||||||
# from the repo root
|
# from the repo root
|
||||||
docker compose -f docker-dev/docker-compose.yml up -d --build
|
docker compose -f docker-dev/docker-compose.yml up -d --build
|
||||||
|
|
||||||
# wait ~15 seconds for SQL to come up + the cluster to form
|
# wait ~20 seconds for SQL to come up + all three clusters to form
|
||||||
|
|
||||||
open http://localhost # Blazor admin UI via Traefik
|
open http://localhost # main cluster admin UI
|
||||||
open http://localhost:8080 # Traefik dashboard
|
open http://site-a.localhost # site A admin UI
|
||||||
|
open http://site-b.localhost # site B admin UI
|
||||||
|
open http://localhost:8080 # Traefik dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On macOS, `*.localhost` resolves to `127.0.0.1` automatically. On Linux add `127.0.0.1 site-a.localhost site-b.localhost` to `/etc/hosts` if your resolver doesn't.
|
||||||
|
|
||||||
The first build takes a few minutes (.NET SDK image + restore + publish). Subsequent rebuilds are faster with Docker's layer cache.
|
The first build takes a few minutes (.NET SDK image + restore + publish). Subsequent rebuilds are faster with Docker's layer cache.
|
||||||
|
|
||||||
## Auth (dev only)
|
## Auth (dev only)
|
||||||
@@ -58,5 +105,8 @@ The `-v` drops the SQL + LDAP volumes; remove it to keep ConfigDb state across r
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- This compose is for the **local Mac/Linux developer rig**. The team's CI + soak runs go to the remote docker host at `10.100.0.35` (see `docs/v2/dev-environment.md`); the file there mirrors this one with adjusted port bindings.
|
- This compose is for the **local Mac/Linux developer rig**. The team's CI + soak runs go to the remote docker host at `10.100.0.35` (see `docs/v2/dev-environment.md`); the file there mirrors this one with adjusted port bindings.
|
||||||
- The OPC UA driver endpoints (`opc.tcp://localhost:4840`, `opc.tcp://localhost:4841`) are reachable directly from the host — Traefik is only in front of the admin HTTP surface.
|
- The OPC UA driver endpoints are reachable directly from the host (Traefik is only in front of the admin HTTP surface):
|
||||||
|
- Main: `opc.tcp://localhost:4840` (driver-a), `opc.tcp://localhost:4841` (driver-b)
|
||||||
|
- Site A: `opc.tcp://localhost:4842` (site-a-1), `opc.tcp://localhost:4843` (site-a-2)
|
||||||
|
- Site B: `opc.tcp://localhost:4844` (site-b-1), `opc.tcp://localhost:4845` (site-b-2)
|
||||||
- Galaxy + Wonderware drivers can't run in Linux containers (they need the Windows-only mxaccessgw + Historian SDK). On non-Windows, `DriverInstanceActor.ShouldStub(driverType, roles)` returns `true` for those types and the actor goes straight to a `Stubbed` state that returns deterministic success.
|
- Galaxy + Wonderware drivers can't run in Linux containers (they need the Windows-only mxaccessgw + Historian SDK). On non-Windows, `DriverInstanceActor.ShouldStub(driverType, roles)` returns `true` for those types and the actor goes straight to a `Stubbed` state that returns deterministic success.
|
||||||
|
|||||||
+149
-11
@@ -1,18 +1,41 @@
|
|||||||
# docker-dev/ — Mac-friendly four-node fleet for v2 development + manual UI exercise.
|
# docker-dev/ — Mac-friendly multi-cluster fleet for v2 development + manual UI exercise.
|
||||||
#
|
#
|
||||||
# Stack:
|
# Stack (3 separate Akka clusters — all share the single `OtOpcUa` ConfigDb):
|
||||||
# sql SQL Server 2022 (ConfigDb backing store)
|
# 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
|
# ldap OpenLDAP with the dev users from C:\publish\glauth\auth.md mirrored in
|
||||||
# admin-a OtOpcUa.Host with OTOPCUA_ROLES=admin (cluster seed)
|
#
|
||||||
# admin-b OtOpcUa.Host with OTOPCUA_ROLES=admin (joins admin-a)
|
# Main cluster (existing — split-role admin / driver pair on a single Akka mesh):
|
||||||
# driver-a OtOpcUa.Host with OTOPCUA_ROLES=driver (joins via admin-a)
|
# admin-a OtOpcUa.Host with OTOPCUA_ROLES=admin (seed)
|
||||||
# driver-b OtOpcUa.Host with OTOPCUA_ROLES=driver (joins via admin-a)
|
# admin-b OtOpcUa.Host with OTOPCUA_ROLES=admin (joins admin-a)
|
||||||
# traefik Routes :80 to whichever admin-* currently passes /health/active
|
# 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:
|
# Usage:
|
||||||
# docker compose -f docker-dev/docker-compose.yml up -d --build
|
# docker compose -f docker-dev/docker-compose.yml up -d --build
|
||||||
# open http://localhost # Blazor admin UI via Traefik
|
# open http://localhost # main cluster Blazor admin UI
|
||||||
# open http://localhost:8080 # Traefik dashboard
|
# open http://site-a.localhost # site A admin UI
|
||||||
|
# open http://site-b.localhost # site B admin UI
|
||||||
|
# open http://localhost:8080 # Traefik dashboard
|
||||||
#
|
#
|
||||||
# Tear-down: docker compose -f docker-dev/docker-compose.yml down -v
|
# Tear-down: docker compose -f docker-dev/docker-compose.yml down -v
|
||||||
|
|
||||||
@@ -34,6 +57,20 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 20
|
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"
|
||||||
|
|
||||||
ldap:
|
ldap:
|
||||||
image: bitnami/openldap:2.6
|
image: bitnami/openldap:2.6
|
||||||
environment:
|
environment:
|
||||||
@@ -113,6 +150,103 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "4841:4840"
|
- "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__Server: "ldap"
|
||||||
|
Authentication__Ldap__Port: "1389"
|
||||||
|
Authentication__Ldap__AllowInsecureLdap: "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__Server: "ldap"
|
||||||
|
Authentication__Ldap__Port: "1389"
|
||||||
|
Authentication__Ldap__AllowInsecureLdap: "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__Server: "ldap"
|
||||||
|
Authentication__Ldap__Port: "1389"
|
||||||
|
Authentication__Ldap__AllowInsecureLdap: "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__Server: "ldap"
|
||||||
|
Authentication__Ldap__Port: "1389"
|
||||||
|
Authentication__Ldap__AllowInsecureLdap: "true"
|
||||||
|
ports:
|
||||||
|
- "4845:4840"
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.1
|
image: traefik:v3.1
|
||||||
command:
|
command:
|
||||||
@@ -128,3 +262,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- admin-a
|
- admin-a
|
||||||
- admin-b
|
- admin-b
|
||||||
|
- site-a-1
|
||||||
|
- site-a-2
|
||||||
|
- site-b-1
|
||||||
|
- site-b-2
|
||||||
|
|||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# docker-dev cluster-seed entrypoint. Waits for the host containers to finish
|
||||||
|
# their EF Core auto-migration (which creates the ServerCluster table), then
|
||||||
|
# applies the idempotent seed script.
|
||||||
|
#
|
||||||
|
# Image: mcr.microsoft.com/mssql-tools (Debian + sqlcmd at /opt/mssql-tools18/bin).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SQLCMD="/opt/mssql-tools18/bin/sqlcmd"
|
||||||
|
SERVER="${SQL_HOST:-sql},1433"
|
||||||
|
USER="${SQL_USER:-sa}"
|
||||||
|
PASS="${SQL_PASSWORD:-OtOpcUa!Dev123}"
|
||||||
|
DB="${SQL_DATABASE:-OtOpcUa}"
|
||||||
|
|
||||||
|
run_sql() {
|
||||||
|
"$SQLCMD" -S "$SERVER" -U "$USER" -P "$PASS" -d "$DB" -No -b -h -1 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[cluster-seed] waiting for SQL Server to accept connections..."
|
||||||
|
until run_sql -Q "SELECT 1" >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "[cluster-seed] SQL Server up."
|
||||||
|
|
||||||
|
echo "[cluster-seed] waiting for $DB.ServerCluster (host containers must finish EF migration)..."
|
||||||
|
until run_sql -Q "IF OBJECT_ID('dbo.ServerCluster') IS NULL THROW 50001, 'missing', 1; SELECT 1" >/dev/null 2>&1; do
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "[cluster-seed] schema ready."
|
||||||
|
|
||||||
|
echo "[cluster-seed] applying seed-clusters.sql..."
|
||||||
|
run_sql -i /seed/seed-clusters.sql
|
||||||
|
|
||||||
|
echo "[cluster-seed] done."
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
-- docker-dev cluster seed. Idempotent — safe to re-run on every `docker compose up`.
|
||||||
|
--
|
||||||
|
-- Populates:
|
||||||
|
-- ServerCluster MAIN, SITE-A, SITE-B
|
||||||
|
-- ClusterNode driver-a, driver-b → MAIN
|
||||||
|
-- site-a-1, site-a-2 → SITE-A
|
||||||
|
-- site-b-1, site-b-2 → SITE-B
|
||||||
|
--
|
||||||
|
-- ServerCluster.NodeCount + RedundancyMode are coupled by CHECK constraint:
|
||||||
|
-- NodeCount=1 ⇒ RedundancyMode='None'
|
||||||
|
-- NodeCount=2 ⇒ RedundancyMode∈('Warm','Hot')
|
||||||
|
--
|
||||||
|
-- Each ClusterNode.ApplicationUri MUST be globally unique (UX_ClusterNode_ApplicationUri).
|
||||||
|
-- Convention: urn:OtOpcUa:<NodeId>.
|
||||||
|
--
|
||||||
|
-- Host = Compose service name (resolves inside the otopcua-dev network).
|
||||||
|
-- OpcUaPort stays at the container-internal 4840; the host-side port mapping is in
|
||||||
|
-- docker-compose.yml ports: blocks and is irrelevant to ClusterNode rows.
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- ServerCluster
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ServerCluster WHERE ClusterId = 'MAIN')
|
||||||
|
INSERT INTO dbo.ServerCluster
|
||||||
|
(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, Notes, CreatedBy)
|
||||||
|
VALUES
|
||||||
|
('MAIN', 'Main cluster', 'zb', 'docker-dev',
|
||||||
|
2, 'Warm', 1,
|
||||||
|
'docker-dev seed — admin-a/admin-b control-plane, driver-a/driver-b OPC UA publishers.',
|
||||||
|
'docker-dev-seed');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ServerCluster WHERE ClusterId = 'SITE-A')
|
||||||
|
INSERT INTO dbo.ServerCluster
|
||||||
|
(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, Notes, CreatedBy)
|
||||||
|
VALUES
|
||||||
|
('SITE-A', 'Site A', 'zb', 'site-a',
|
||||||
|
2, 'Warm', 1,
|
||||||
|
'docker-dev seed — 2-node fused admin+driver cluster.',
|
||||||
|
'docker-dev-seed');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ServerCluster WHERE ClusterId = 'SITE-B')
|
||||||
|
INSERT INTO dbo.ServerCluster
|
||||||
|
(ClusterId, Name, Enterprise, Site, NodeCount, RedundancyMode, Enabled, Notes, CreatedBy)
|
||||||
|
VALUES
|
||||||
|
('SITE-B', 'Site B', 'zb', 'site-b',
|
||||||
|
2, 'Warm', 1,
|
||||||
|
'docker-dev seed — 2-node fused admin+driver cluster.',
|
||||||
|
'docker-dev-seed');
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- ClusterNode — main cluster OPC UA publishers
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'driver-a')
|
||||||
|
INSERT INTO dbo.ClusterNode
|
||||||
|
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||||
|
VALUES ('driver-a', 'MAIN', 'driver-a', 4840, 8081, 'urn:OtOpcUa:driver-a', 200, 1, 'docker-dev-seed');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'driver-b')
|
||||||
|
INSERT INTO dbo.ClusterNode
|
||||||
|
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||||
|
VALUES ('driver-b', 'MAIN', 'driver-b', 4840, 8081, 'urn:OtOpcUa:driver-b', 150, 1, 'docker-dev-seed');
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- ClusterNode — site A
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-1')
|
||||||
|
INSERT INTO dbo.ClusterNode
|
||||||
|
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||||
|
VALUES ('site-a-1', 'SITE-A', 'site-a-1', 4840, 8081, 'urn:OtOpcUa:site-a-1', 200, 1, 'docker-dev-seed');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-a-2')
|
||||||
|
INSERT INTO dbo.ClusterNode
|
||||||
|
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||||
|
VALUES ('site-a-2', 'SITE-A', 'site-a-2', 4840, 8081, 'urn:OtOpcUa:site-a-2', 150, 1, 'docker-dev-seed');
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- ClusterNode — site B
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-1')
|
||||||
|
INSERT INTO dbo.ClusterNode
|
||||||
|
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||||
|
VALUES ('site-b-1', 'SITE-B', 'site-b-1', 4840, 8081, 'urn:OtOpcUa:site-b-1', 200, 1, 'docker-dev-seed');
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM dbo.ClusterNode WHERE NodeId = 'site-b-2')
|
||||||
|
INSERT INTO dbo.ClusterNode
|
||||||
|
(NodeId, ClusterId, Host, OpcUaPort, DashboardPort, ApplicationUri, ServiceLevelBase, Enabled, CreatedBy)
|
||||||
|
VALUES ('site-b-2', 'SITE-B', 'site-b-2', 4840, 8081, 'urn:OtOpcUa:site-b-2', 150, 1, 'docker-dev-seed');
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- Summary (logged by sqlcmd output)
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SELECT ClusterId, Name, NodeCount, RedundancyMode FROM dbo.ServerCluster ORDER BY ClusterId;
|
||||||
|
SELECT NodeId, ClusterId, Host, OpcUaPort, ApplicationUri, ServiceLevelBase
|
||||||
|
FROM dbo.ClusterNode ORDER BY ClusterId, NodeId;
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
# docker-dev companion to scripts/install/traefik-dynamic.yml. Same routing rules,
|
# docker-dev companion to scripts/install/traefik-dynamic.yml. Routes three
|
||||||
# but the upstream targets are the Compose service names (admin-a, admin-b) on
|
# Akka clusters that share the Compose network:
|
||||||
# port 9000 instead of the Windows hostnames a bare-metal deployment would use.
|
#
|
||||||
|
# - Main cluster (default): PathPrefix(`/`) → admin-a / admin-b.
|
||||||
|
# - Site A cluster: Host(`site-a.localhost`) → site-a-1 / site-a-2.
|
||||||
|
# - Site B cluster: Host(`site-b.localhost`) → site-b-1 / site-b-2.
|
||||||
|
#
|
||||||
|
# Host-header rules are more specific than PathPrefix, so they win over the
|
||||||
|
# default router for the site hostnames automatically — no priority field needed.
|
||||||
|
|
||||||
http:
|
http:
|
||||||
routers:
|
routers:
|
||||||
@@ -9,6 +15,16 @@ http:
|
|||||||
rule: "PathPrefix(`/`)"
|
rule: "PathPrefix(`/`)"
|
||||||
service: otopcua-admin
|
service: otopcua-admin
|
||||||
|
|
||||||
|
otopcua-site-a:
|
||||||
|
entryPoints: ["web"]
|
||||||
|
rule: "Host(`site-a.localhost`)"
|
||||||
|
service: otopcua-site-a
|
||||||
|
|
||||||
|
otopcua-site-b:
|
||||||
|
entryPoints: ["web"]
|
||||||
|
rule: "Host(`site-b.localhost`)"
|
||||||
|
service: otopcua-site-b
|
||||||
|
|
||||||
services:
|
services:
|
||||||
otopcua-admin:
|
otopcua-admin:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
@@ -19,3 +35,23 @@ http:
|
|||||||
path: /health/active
|
path: /health/active
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
|
|
||||||
|
otopcua-site-a:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://site-a-1:9000"
|
||||||
|
- url: "http://site-a-2:9000"
|
||||||
|
healthCheck:
|
||||||
|
path: /health/active
|
||||||
|
interval: 5s
|
||||||
|
timeout: 2s
|
||||||
|
|
||||||
|
otopcua-site-b:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://site-b-1:9000"
|
||||||
|
- url: "http://site-b-2:9000"
|
||||||
|
healthCheck:
|
||||||
|
path: /health/active
|
||||||
|
interval: 5s
|
||||||
|
timeout: 2s
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@page "/alerts"
|
@page "/alerts"
|
||||||
@* Live alarm tail via SignalR. Subscribes to /hubs/alerts and shows the most-recent
|
@* Live alarm tail via SignalR. Subscribes to /hubs/alerts and shows the most-recent
|
||||||
AlarmTransitionEvent entries. Engine wiring (ScriptedAlarmActor publish on the `alerts`
|
AlarmTransitionEvent entries published by ScriptedAlarmActor (Runtime/ScriptedAlarms)
|
||||||
topic) lands with F9; until then the connection stays open and the table is empty. *@
|
and the AB CIP ALMD bridge. *@
|
||||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||||
@rendermode RenderMode.InteractiveServer
|
@rendermode RenderMode.InteractiveServer
|
||||||
@using Microsoft.AspNetCore.SignalR.Client
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
@@ -23,14 +23,14 @@
|
|||||||
<section class="panel notice rise" style="animation-delay:.02s">
|
<section class="panel notice rise" style="animation-delay:.02s">
|
||||||
Live alarm transitions from the cluster's <span class="mono">alerts</span> DPS topic. Shows
|
Live alarm transitions from the cluster's <span class="mono">alerts</span> DPS topic. Shows
|
||||||
the most-recent @Capacity entries since the page opened; reload for a fresh window. Sources:
|
the most-recent @Capacity entries since the page opened; reload for a fresh window. Sources:
|
||||||
ScriptedAlarmActor, native AB CIP ALMD bridge (F9), Galaxy alarm bridge (future).
|
ScriptedAlarmActor, native driver alarm bridges (AB CIP ALMD, Galaxy where wired).
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@if (_rows.Count == 0)
|
@if (_rows.Count == 0)
|
||||||
{
|
{
|
||||||
<section class="panel notice rise mt-3" style="animation-delay:.08s">
|
<section class="panel notice rise mt-3" style="animation-delay:.08s">
|
||||||
No alarms yet. Engine wiring (F9 ScriptedAlarmActor) is pending; once it ships the table
|
No alarms in the current window. The table will populate as soon as a
|
||||||
below will start populating in real time.
|
ScriptedAlarmActor or driver alarm bridge publishes a transition.
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user