refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -2,7 +2,7 @@
**Date:** 2026-05-24
**Status:** Approved — ready for implementation plan
**Purpose:** Stand up a second, concurrently-running ScadaLink cluster on the same machine so the new Transport (#24) feature can be exercised end-to-end against a real second environment (export from one UI, import into the other).
**Purpose:** Stand up a second, concurrently-running ScadaBridge cluster on the same machine so the new Transport (#24) feature can be exercised end-to-end against a real second environment (export from one UI, import into the other).
## Goal
@@ -28,39 +28,39 @@ A sibling `docker-env2/` directory with `deploy.sh` / `teardown.sh` / `seed-site
└─────────────┬──────────────────────┘ │ 9123/9124 gRPC) │
│ └──────────┬───────────────────┘
│ │
▼ scadalink-net (shared bridge network) ◄──────┘
▼ scadabridge-net (shared bridge network) ◄──────┘
┌──────────────────────────────────────────────────────────────┐
│ scadalink-mssql ScadaLinkConfig (primary DB) │
│ ScadaLinkMachineData (primary DB) │
│ ScadaLinkConfig2 (env2 DB) ← new │
│ ScadaLinkMachineData2(env2 DB) ← new │
│ scadalink-ldap (shared — same test users) │
│ scadalink-smtp (shared Mailpit) │
│ scadalink-opcua (shared) │
│ scadalink-restapi (shared) │
│ scadabridge-mssql ScadaBridgeConfig (primary DB) │
│ ScadaBridgeMachineData (primary DB) │
│ ScadaBridgeConfig2 (env2 DB) ← new │
│ ScadaBridgeMachineData2(env2 DB) ← new │
│ scadabridge-ldap (shared — same test users) │
│ scadabridge-smtp (shared Mailpit) │
│ scadabridge-opcua (shared) │
│ scadabridge-restapi (shared) │
└──────────────────────────────────────────────────────────────┘
```
Both stacks attach to the same `scadalink-net` Docker bridge so env2's app containers can reach the infra services by container hostname (`scadalink-mssql`, `scadalink-ldap`, etc.). Akka clusters are independent — each side's `SeedNodes` lists only its own central nodes, so they never gossip-merge despite sharing the network.
Both stacks attach to the same `scadabridge-net` Docker bridge so env2's app containers can reach the infra services by container hostname (`scadabridge-mssql`, `scadabridge-ldap`, etc.). Akka clusters are independent — each side's `SeedNodes` lists only its own central nodes, so they never gossip-merge despite sharing the network.
## Topology & Port Allocation
| Role | Container name | Host Web | Host Akka | Host gRPC | Notes |
|----------------|-----------------------------|----------|-----------|-----------|-------|
| Traefik LB | `scadalink-env2-traefik` | 9100 | — | — | Dashboard on host 8181 |
| Central A | `scadalink-env2-central-a` | 9101 | 9111 | — | |
| Central B | `scadalink-env2-central-b` | 9102 | 9112 | — | |
| Site-X A | `scadalink-env2-site-x-a` | — | 9121 | 9123 | |
| Site-X B | `scadalink-env2-site-x-b` | — | 9122 | 9124 | |
| Traefik LB | `scadabridge-env2-traefik` | 9100 | — | — | Dashboard on host 8181 |
| Central A | `scadabridge-env2-central-a` | 9101 | 9111 | — | |
| Central B | `scadabridge-env2-central-b` | 9102 | 9112 | — | |
| Site-X A | `scadabridge-env2-site-x-a` | — | 9121 | 9123 | |
| Site-X B | `scadabridge-env2-site-x-b` | — | 9122 | 9124 | |
Pattern: env2 host ports are primary + 100 (e.g. primary central-a 9001 → env2 central-a 9101). Confirmed free at design time. Identifier `site-x` distinguishes env2's single site from primary's `site-a/b/c` in logs/UI (technically not required — each central has its own ConfigurationDB — but clearer for operators).
## Infrastructure & Databases
**Shared `scadalink-net` + shared `scadalink-mssql` container, separate logical databases:**
**Shared `scadabridge-net` + shared `scadabridge-mssql` container, separate logical databases:**
- New databases: `ScadaLinkConfig2`, `ScadaLinkMachineData2`.
- Reuse the existing `scadalink_app` SQL login with `db_owner` on both — one credential to manage.
- New databases: `ScadaBridgeConfig2`, `ScadaBridgeMachineData2`.
- Reuse the existing `scadabridge_app` SQL login with `db_owner` on both — one credential to manage.
- DB creation handled by a new `infra/mssql/setup-env2.sql` (idempotent, `IF NOT EXISTS`-guarded).
- Two activation paths:
1. **Fresh MSSQL volume** — mount `setup-env2.sql` alongside the existing `setup.sql` in `/docker-entrypoint-initdb.d/` so it runs automatically on first startup.
@@ -71,8 +71,8 @@ Pattern: env2 host ports are primary + 100 (e.g. primary central-a 9001 → env2
Connection strings in env2 central appsettings:
```
ConfigurationDb: Server=scadalink-mssql,1433;Database=ScadaLinkConfig2;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true
MachineDataDb: Server=scadalink-mssql,1433;Database=ScadaLinkMachineData2;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true
ConfigurationDb: Server=scadabridge-mssql,1433;Database=ScadaBridgeConfig2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true
MachineDataDb: Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData2;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true
```
## Directory Layout
@@ -83,7 +83,7 @@ docker-env2/
├── deploy.sh # build (reuses docker/build.sh) + init-db + compose up
├── teardown.sh # compose down (preserves data + logs)
├── seed-sites.sh # CLI creates site-x against http://localhost:9100
├── init-db.sh # sqlcmd exec against scadalink-mssql
├── init-db.sh # sqlcmd exec against scadabridge-mssql
├── central-node-a/
│ └── appsettings.Central.json
├── central-node-b/
@@ -98,7 +98,7 @@ docker-env2/
│ └── logs/
└── traefik/
├── traefik.yml # dashboard on :8080 (host 8181)
└── dynamic.yml # service URLs → scadalink-env2-central-a/b
└── dynamic.yml # service URLs → scadabridge-env2-central-a/b
```
Mirrors `docker/`'s shape exactly so operator muscle memory carries over.
@@ -109,12 +109,12 @@ Each env2 appsettings file is a near-clone of the primary equivalent with these
| Field | Primary | Env2 |
|--------------------------------------------|-----------------------------------------------------|-----------------------------------------------------|
| `Node.NodeHostname` | `scadalink-central-a` / `scadalink-site-a-a` / ... | `scadalink-env2-central-a` / `scadalink-env2-site-x-a` / ... |
| `Node.NodeHostname` | `scadabridge-central-a` / `scadabridge-site-a-a` / ... | `scadabridge-env2-central-a` / `scadabridge-env2-site-x-a` / ... |
| `Cluster.SeedNodes` | primary central hostnames | env2 central hostnames |
| `Communication.CentralContactPoints` (site)| primary central hostnames | env2 central hostnames |
| `Node.SiteId` (site) | `site-a` / `site-b` / `site-c` | `site-x` |
| `Database.ConfigurationDb` | `ScadaLinkConfig` | `ScadaLinkConfig2` |
| `Database.MachineDataDb` | `ScadaLinkMachineData` | `ScadaLinkMachineData2` |
| `Database.ConfigurationDb` | `ScadaBridgeConfig` | `ScadaBridgeConfig2` |
| `Database.MachineDataDb` | `ScadaBridgeMachineData` | `ScadaBridgeMachineData2` |
| `Notification.FromAddress` | `scada-notifications@company.com` | `scada-notifications-env2@company.com` |
| `Security.JwtSigningKey` | primary signing key | a distinct env2 signing key |
| `Transport.SourceEnvironment` | `docker-cluster` | `docker-cluster-env2` |
@@ -129,12 +129,12 @@ Each env2 appsettings file is a near-clone of the primary equivalent with these
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== ScadaLink Env2 Docker Deploy ==="
echo "=== ScadaBridge Env2 Docker Deploy ==="
# Reuse the primary build (same scadalink:latest image)
# Reuse the primary build (same scadabridge:latest image)
"$SCRIPT_DIR/../docker/build.sh"
# Ensure env2 databases exist on the shared scadalink-mssql
# Ensure env2 databases exist on the shared scadabridge-mssql
"$SCRIPT_DIR/init-db.sh"
echo "Deploying env2 containers..."
@@ -155,14 +155,14 @@ echo "Seed site: docker-env2/seed-sites.sh"
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if ! docker ps --format '{{.Names}}' | grep -q '^scadalink-mssql$'; then
echo "ERROR: scadalink-mssql is not running. Start it: cd infra && docker compose up -d" >&2
if ! docker ps --format '{{.Names}}' | grep -q '^scadabridge-mssql$'; then
echo "ERROR: scadabridge-mssql is not running. Start it: cd infra && docker compose up -d" >&2
exit 1
fi
echo "Applying env2 database setup..."
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
docker exec -i scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
< "$SCRIPT_DIR/../infra/mssql/setup-env2.sql"
echo "Env2 databases ready."
@@ -174,7 +174,7 @@ echo "Env2 databases ready."
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CLI="dotnet run --project $PROJECT_ROOT/src/ScadaLink.CLI --"
CLI="dotnet run --project $PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI --"
AUTH="--username multi-role --password password"
URL="--url http://localhost:9100"
@@ -183,10 +183,10 @@ $CLI $URL $AUTH site create \
--name "Env2 Site X" \
--identifier "site-x" \
--description "Env2 test site - two-node cluster" \
--node-a-address "akka.tcp://scadalink@scadalink-env2-site-x-a:8082" \
--node-b-address "akka.tcp://scadalink@scadalink-env2-site-x-b:8082" \
--grpc-node-a-address "http://scadalink-env2-site-x-a:8083" \
--grpc-node-b-address "http://scadalink-env2-site-x-b:8083" \
--node-a-address "akka.tcp://scadabridge@scadabridge-env2-site-x-a:8082" \
--node-b-address "akka.tcp://scadabridge@scadabridge-env2-site-x-b:8082" \
--grpc-node-a-address "http://scadabridge-env2-site-x-a:8083" \
--grpc-node-b-address "http://scadabridge-env2-site-x-b:8083" \
|| echo " (Site-X may already exist)"
```
@@ -205,7 +205,7 @@ docker compose -f "$SCRIPT_DIR/docker-compose.yml" down
| First-time env2 bring-up | `bash docker-env2/deploy.sh && bash docker-env2/seed-sites.sh` |
| Iterate on env2 after code edit | `bash docker-env2/deploy.sh` |
| Iterate on both envs | `bash docker/deploy.sh && bash docker-env2/deploy.sh` (build cached on 2nd) |
| Wipe env2 DB for clean re-import | `docker exec scadalink-mssql sqlcmd ... DROP DATABASE ScadaLinkConfig2; DROP DATABASE ScadaLinkMachineData2;` then `bash docker-env2/deploy.sh` |
| Wipe env2 DB for clean re-import | `docker exec scadabridge-mssql sqlcmd ... DROP DATABASE ScadaBridgeConfig2; DROP DATABASE ScadaBridgeMachineData2;` then `bash docker-env2/deploy.sh` |
| Stop env2 only | `bash docker-env2/teardown.sh` |
## Transport Testing Workflow — The Whole Point of env2
@@ -233,11 +233,11 @@ docker compose -f "$SCRIPT_DIR/docker-compose.yml" down
## Error Handling & Edge Cases
- **`init-db.sh`** fails fast with a clear message if `scadalink-mssql` isn't running.
- **`init-db.sh`** fails fast with a clear message if `scadabridge-mssql` isn't running.
- **`deploy.sh`** runs with `set -euo pipefail` so any failed step halts cleanly.
- **MSSQL volume reset** — both the docker-entrypoint mount and the exec-based `init-db.sh` apply the same idempotent script; either path leaves env2 DBs ready.
- **Cluster cross-talk** — primary and env2 use the same Akka system name `scadalink` but disjoint seed-node hostnames, so the gossip protocols cannot merge. Defensive: env2 appsettings are written from scratch, not sed'd from primary.
- **gRPC streaming** — env2 central uses container-name DNS (`http://scadalink-env2-site-x-a:8083`) for site-x streams, populated by `seed-sites.sh`.
- **Cluster cross-talk** — primary and env2 use the same Akka system name `scadabridge` but disjoint seed-node hostnames, so the gossip protocols cannot merge. Defensive: env2 appsettings are written from scratch, not sed'd from primary.
- **gRPC streaming** — env2 central uses container-name DNS (`http://scadabridge-env2-site-x-a:8083`) for site-x streams, populated by `seed-sites.sh`.
- **Cookie/JWT bleed** — different `JwtSigningKey` + different host origins (`localhost:9000` vs `localhost:9100`) mean sessions cannot cross envs.
- **Port collision** — host port range `91XX` non-overlapping with primary's `90XX`; confirmed all 10 ports free at design time. If an operator later remaps, Compose surfaces `bind: address already in use`.
@@ -246,7 +246,7 @@ docker compose -f "$SCRIPT_DIR/docker-compose.yml" down
**No new automated tests are added.** This is infrastructure tooling — the Transport feature already has 39 unit + 26 integration tests. The deliverable is a manual verification checklist at `docs/plans/2026-05-24-second-environment-verification.md` mirroring the Transport manual checklist, walking through the Section 5 golden path.
**First-deploy smoke test:**
1. `docker ps` shows 5 new `scadalink-env2-*` containers.
1. `docker ps` shows 5 new `scadabridge-env2-*` containers.
2. `curl http://localhost:9101/health/ready` returns green.
3. `curl http://localhost:9100/health/active` Traefik routes to active node.
4. Browser to http://localhost:9100 → `multi-role` login → Dashboard renders, Sites page is empty.
@@ -270,7 +270,7 @@ docker compose -f "$SCRIPT_DIR/docker-compose.yml" down
- [ ] `bash docker-env2/deploy.sh` brings up 5 containers cleanly on a machine where primary is already running.
- [ ] `bash docker-env2/seed-sites.sh` registers `site-x` and the site cluster reaches healthy state.
- [ ] http://localhost:9100 serves the env2 Central UI through Traefik with failover between 9101/9102.
- [ ] env2 reads/writes only `ScadaLinkConfig2` / `ScadaLinkMachineData2`; primary's DBs untouched after env2 deploy.
- [ ] env2 reads/writes only `ScadaBridgeConfig2` / `ScadaBridgeMachineData2`; primary's DBs untouched after env2 deploy.
- [ ] `bash docker/deploy.sh && bash docker-env2/deploy.sh` succeeds in sequence; both stacks run concurrently.
- [ ] A bundle exported from primary can be imported into env2, with audit rows tagged by `BundleImportId` and visible in env2's Configuration Audit Log.
- [ ] Manual verification checklist completes end-to-end.