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
+21 -21
View File
@@ -1,6 +1,6 @@
# ScadaLink Env2 Docker Infrastructure
# ScadaBridge Env2 Docker Infrastructure
A second Docker deployment of a minimal ScadaLink cluster topology, designed to run **concurrently with** the primary `docker/` stack so the Transport (#24) feature can be exercised end-to-end across two real environments.
A second Docker deployment of a minimal ScadaBridge cluster topology, designed to run **concurrently with** the primary `docker/` stack so the Transport (#24) feature can be exercised end-to-end across two real environments.
See [`docs/plans/2026-05-24-second-environment-design.md`](../docs/plans/2026-05-24-second-environment-design.md) for the design rationale.
@@ -40,30 +40,30 @@ Env2 host ports are the primary's ports + 100. Both stacks can run simultaneousl
| Node | Container Name | Host Web | Host Akka | Host gRPC | Internal |
|------|----------------|----------|-----------|-----------|----------|
| Traefik LB | `scadalink-env2-traefik` | 9100 | — | — | 80 (proxy), 8080 (dashboard host:8181) |
| Central A | `scadalink-env2-central-a` | 9101 | 9111 | — | 5000 (web), 8081 (Akka) |
| Central B | `scadalink-env2-central-b` | 9102 | 9112 | — | 5000 (web), 8081 (Akka) |
| Site-X A | `scadalink-env2-site-x-a` | — | 9121 | 9123 | 8082 (Akka), 8083 (gRPC) |
| Site-X B | `scadalink-env2-site-x-b` | — | 9122 | 9124 | 8082 (Akka), 8083 (gRPC) |
| Traefik LB | `scadabridge-env2-traefik` | 9100 | — | — | 80 (proxy), 8080 (dashboard host:8181) |
| Central A | `scadabridge-env2-central-a` | 9101 | 9111 | — | 5000 (web), 8081 (Akka) |
| Central B | `scadabridge-env2-central-b` | 9102 | 9112 | — | 5000 (web), 8081 (Akka) |
| Site-X A | `scadabridge-env2-site-x-a` | — | 9121 | 9123 | 8082 (Akka), 8083 (gRPC) |
| Site-X B | `scadabridge-env2-site-x-b` | — | 9122 | 9124 | 8082 (Akka), 8083 (gRPC) |
## Shared Infrastructure
Env2 attaches to the existing `scadalink-net` Docker bridge network and reuses these primary infra containers:
Env2 attaches to the existing `scadabridge-net` Docker bridge network and reuses these primary infra containers:
| Service | Container | What env2 uses it for |
|---------|-----------|-----------------------|
| MS SQL | `scadalink-mssql` | Env2-specific databases `ScadaLinkConfig2` / `ScadaLinkMachineData2` |
| LDAP | `scadalink-ldap` | Authentication (same test users) |
| SMTP | `scadalink-smtp` | Notification capture in Mailpit (env2 emails distinguishable by `FromAddress`) |
| OPC UA | `scadalink-opcua` | Simulated tags for site-x data connections |
| REST API | `scadalink-restapi` | External REST API testing |
| MS SQL | `scadabridge-mssql` | Env2-specific databases `ScadaBridgeConfig2` / `ScadaBridgeMachineData2` |
| LDAP | `scadabridge-ldap` | Authentication (same test users) |
| SMTP | `scadabridge-smtp` | Notification capture in Mailpit (env2 emails distinguishable by `FromAddress`) |
| OPC UA | `scadabridge-opcua` | Simulated tags for site-x data connections |
| REST API | `scadabridge-restapi` | External REST API testing |
## Commands
### First-Time Setup
```bash
# 1. Make sure primary infra is up (creates scadalink-net, scadalink-mssql, etc.)
# 1. Make sure primary infra is up (creates scadabridge-net, scadabridge-mssql, etc.)
cd infra && docker compose up -d && cd ..
# 2. Build image + create env2 databases + deploy env2 containers
@@ -101,15 +101,15 @@ bash docker-env2/teardown.sh
Containers stop, volumes (data + logs) preserved. To also drop the env2 databases:
```bash
docker exec scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
-Q "DROP DATABASE ScadaLinkConfig2; DROP DATABASE ScadaLinkMachineData2;"
docker exec scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
-Q "DROP DATABASE ScadaBridgeConfig2; DROP DATABASE ScadaBridgeMachineData2;"
```
### CLI Access
```bash
dotnet run --project src/ScadaLink.CLI -- \
dotnet run --project src/ZB.MOM.WW.ScadaBridge.CLI -- \
--url http://localhost:9100 \
--username multi-role --password password \
template list
@@ -119,7 +119,7 @@ dotnet run --project src/ScadaLink.CLI -- \
```bash
docker compose -f docker-env2/docker-compose.yml logs -f
docker logs -f scadalink-env2-central-a
docker logs -f scadabridge-env2-central-a
```
## Test Users
@@ -134,7 +134,7 @@ See [`docs/plans/2026-05-24-second-environment-verification.md`](../docs/plans/2
- Single site (`site-x`) instead of three (`site-a/b/c`).
- Host port range 91XX vs primary 90XX.
- Container names prefixed `scadalink-env2-`.
- Databases `ScadaLinkConfig2` / `ScadaLinkMachineData2` on the **shared** `scadalink-mssql`.
- Container names prefixed `scadabridge-env2-`.
- Databases `ScadaBridgeConfig2` / `ScadaBridgeMachineData2` on the **shared** `scadabridge-mssql`.
- `Transport.SourceEnvironment` = `"docker-cluster-env2"` (stamped into exported bundle manifests).
- Distinct `Security.JwtSigningKey` (sessions cannot cross envs).
@@ -1,15 +1,15 @@
{
"ScadaLink": {
"ScadaBridge": {
"Node": {
"Role": "Central",
"NodeName": "central-a",
"NodeHostname": "scadalink-env2-central-a",
"NodeHostname": "scadabridge-env2-central-a",
"RemotingPort": 8081
},
"Cluster": {
"SeedNodes": [
"akka.tcp://scadalink@scadalink-env2-central-a:8081",
"akka.tcp://scadalink@scadalink-env2-central-b:8081"
"akka.tcp://scadabridge@scadabridge-env2-central-a:8081",
"akka.tcp://scadabridge@scadabridge-env2-central-b:8081"
],
"SplitBrainResolverStrategy": "keep-oldest",
"StableAfter": "00:00:15",
@@ -18,18 +18,18 @@
"MinNrOfMembers": 1
},
"Database": {
"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"
},
"Security": {
"LdapServer": "scadalink-ldap",
"LdapServer": "scadabridge-ldap",
"LdapPort": 3893,
"LdapUseTls": false,
"AllowInsecureLdap": true,
"LdapSearchBase": "dc=scadalink,dc=local",
"LdapServiceAccountDn": "cn=admin,dc=scadalink,dc=local",
"LdapSearchBase": "dc=scadabridge,dc=local",
"LdapServiceAccountDn": "cn=admin,dc=scadabridge,dc=local",
"LdapServiceAccountPassword": "password",
"JwtSigningKey": "scadalink-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long",
"JwtSigningKey": "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long",
"JwtExpiryMinutes": 15,
"IdleTimeoutMinutes": 30,
"RequireHttpsCookie": false
@@ -49,7 +49,7 @@
"DefaultMethodTimeout": "00:00:30"
},
"Notification": {
"SmtpServer": "scadalink-smtp",
"SmtpServer": "scadabridge-smtp",
"SmtpPort": 1025,
"AuthMode": "None",
"FromAddress": "scada-notifications-env2@company.com"
@@ -1,15 +1,15 @@
{
"ScadaLink": {
"ScadaBridge": {
"Node": {
"Role": "Central",
"NodeName": "central-b",
"NodeHostname": "scadalink-env2-central-b",
"NodeHostname": "scadabridge-env2-central-b",
"RemotingPort": 8081
},
"Cluster": {
"SeedNodes": [
"akka.tcp://scadalink@scadalink-env2-central-a:8081",
"akka.tcp://scadalink@scadalink-env2-central-b:8081"
"akka.tcp://scadabridge@scadabridge-env2-central-a:8081",
"akka.tcp://scadabridge@scadabridge-env2-central-b:8081"
],
"SplitBrainResolverStrategy": "keep-oldest",
"StableAfter": "00:00:15",
@@ -18,18 +18,18 @@
"MinNrOfMembers": 1
},
"Database": {
"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"
},
"Security": {
"LdapServer": "scadalink-ldap",
"LdapServer": "scadabridge-ldap",
"LdapPort": 3893,
"LdapUseTls": false,
"AllowInsecureLdap": true,
"LdapSearchBase": "dc=scadalink,dc=local",
"LdapServiceAccountDn": "cn=admin,dc=scadalink,dc=local",
"LdapSearchBase": "dc=scadabridge,dc=local",
"LdapServiceAccountDn": "cn=admin,dc=scadabridge,dc=local",
"LdapServiceAccountPassword": "password",
"JwtSigningKey": "scadalink-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long",
"JwtSigningKey": "scadabridge-env2-dev-jwt-signing-key-must-be-at-least-32-characters-long",
"JwtExpiryMinutes": 15,
"IdleTimeoutMinutes": 30,
"RequireHttpsCookie": false
@@ -49,7 +49,7 @@
"DefaultMethodTimeout": "00:00:30"
},
"Notification": {
"SmtpServer": "scadalink-smtp",
"SmtpServer": "scadabridge-smtp",
"SmtpPort": 1025,
"AuthMode": "None",
"FromAddress": "scada-notifications-env2@company.com"
+3 -3
View File
@@ -3,12 +3,12 @@ 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, same network creation)
# Reuse the primary build (same scadabridge:latest image, same network creation)
"$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 ""
+15 -15
View File
@@ -1,7 +1,7 @@
services:
central-a:
image: scadalink:latest
container_name: scadalink-env2-central-a
image: scadabridge:latest
container_name: scadabridge-env2-central-a
environment:
SCADALINK_CONFIG: Central
ASPNETCORE_ENVIRONMENT: Development
@@ -13,12 +13,12 @@ services:
- ./central-node-a/appsettings.Central.json:/app/appsettings.Central.json:ro
- ./central-node-a/logs:/app/logs
networks:
- scadalink-net
- scadabridge-net
restart: unless-stopped
central-b:
image: scadalink:latest
container_name: scadalink-env2-central-b
image: scadabridge:latest
container_name: scadabridge-env2-central-b
environment:
SCADALINK_CONFIG: Central
ASPNETCORE_ENVIRONMENT: Development
@@ -30,12 +30,12 @@ services:
- ./central-node-b/appsettings.Central.json:/app/appsettings.Central.json:ro
- ./central-node-b/logs:/app/logs
networks:
- scadalink-net
- scadabridge-net
restart: unless-stopped
site-x-a:
image: scadalink:latest
container_name: scadalink-env2-site-x-a
image: scadabridge:latest
container_name: scadabridge-env2-site-x-a
environment:
SCADALINK_CONFIG: Site
ports:
@@ -46,12 +46,12 @@ services:
- ./site-x-node-a/data:/app/data
- ./site-x-node-a/logs:/app/logs
networks:
- scadalink-net
- scadabridge-net
restart: unless-stopped
site-x-b:
image: scadalink:latest
container_name: scadalink-env2-site-x-b
image: scadabridge:latest
container_name: scadabridge-env2-site-x-b
environment:
SCADALINK_CONFIG: Site
ports:
@@ -62,12 +62,12 @@ services:
- ./site-x-node-b/data:/app/data
- ./site-x-node-b/logs:/app/logs
networks:
- scadalink-net
- scadabridge-net
restart: unless-stopped
traefik:
image: traefik:v3.4
container_name: scadalink-env2-traefik
container_name: scadabridge-env2-traefik
ports:
- "9100:80" # Env2 central load-balanced entrypoint
- "8181:8080" # Env2 Traefik dashboard
@@ -75,9 +75,9 @@ services:
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
networks:
- scadalink-net
- scadabridge-net
restart: unless-stopped
networks:
scadalink-net:
scadabridge-net:
external: true
+4 -4
View File
@@ -2,14 +2,14 @@
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."
+10 -10
View File
@@ -14,11 +14,11 @@ 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"
echo "=== Seeding ScadaLink Env2 Sites ==="
echo "=== Seeding ScadaBridge Env2 Sites ==="
echo ""
echo "Creating Site-X (Env2 Site X)..."
@@ -26,22 +26,22 @@ $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)"
echo ""
echo "Seeding LDAP group mappings (Design + Deployment)..."
# SecurityConfiguration.HasData declares 4 mappings but the InitialSchema
# migration only inserts the Admin row, so a fresh ScadaLinkConfig2 starts
# migration only inserts the Admin row, so a fresh ScadaBridgeConfig2 starts
# with multi-role getting Admin only -- no Design and no Deployment access.
# Insert the missing three idempotently. (Same divergence exists on the
# primary, but primary's database has the rows from earlier history.)
docker exec -i scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaLink_Dev1#' -C \
-d ScadaLinkConfig2 -Q "
docker exec -i scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P 'ScadaBridge_Dev1#' -C \
-d ScadaBridgeConfig2 -Q "
SET IDENTITY_INSERT LdapGroupMappings ON;
IF NOT EXISTS (SELECT 1 FROM LdapGroupMappings WHERE Id = 2)
INSERT INTO LdapGroupMappings (Id, LdapGroupName, Role) VALUES (2, 'SCADA-Designers', 'Design');
@@ -1,17 +1,17 @@
{
"ScadaLink": {
"ScadaBridge": {
"Node": {
"Role": "Site",
"NodeName": "node-a",
"NodeHostname": "scadalink-env2-site-x-a",
"NodeHostname": "scadabridge-env2-site-x-a",
"SiteId": "site-x",
"RemotingPort": 8082,
"GrpcPort": 8083
},
"Cluster": {
"SeedNodes": [
"akka.tcp://scadalink@scadalink-env2-site-x-a:8082",
"akka.tcp://scadalink@scadalink-env2-site-x-b:8082"
"akka.tcp://scadabridge@scadabridge-env2-site-x-a:8082",
"akka.tcp://scadabridge@scadabridge-env2-site-x-b:8082"
],
"SplitBrainResolverStrategy": "keep-oldest",
"StableAfter": "00:00:15",
@@ -20,7 +20,7 @@
"MinNrOfMembers": 1
},
"Database": {
"SiteDbPath": "/app/data/scadalink.db"
"SiteDbPath": "/app/data/scadabridge.db"
},
"DataConnection": {
"ReconnectInterval": "00:00:05",
@@ -33,8 +33,8 @@
},
"Communication": {
"CentralContactPoints": [
"akka.tcp://scadalink@scadalink-env2-central-a:8081",
"akka.tcp://scadalink@scadalink-env2-central-b:8081"
"akka.tcp://scadabridge@scadabridge-env2-central-a:8081",
"akka.tcp://scadabridge@scadabridge-env2-central-b:8081"
],
"DeploymentTimeout": "00:02:00",
"LifecycleTimeout": "00:00:30",
@@ -1,17 +1,17 @@
{
"ScadaLink": {
"ScadaBridge": {
"Node": {
"Role": "Site",
"NodeName": "node-b",
"NodeHostname": "scadalink-env2-site-x-b",
"NodeHostname": "scadabridge-env2-site-x-b",
"SiteId": "site-x",
"RemotingPort": 8082,
"GrpcPort": 8083
},
"Cluster": {
"SeedNodes": [
"akka.tcp://scadalink@scadalink-env2-site-x-a:8082",
"akka.tcp://scadalink@scadalink-env2-site-x-b:8082"
"akka.tcp://scadabridge@scadabridge-env2-site-x-a:8082",
"akka.tcp://scadabridge@scadabridge-env2-site-x-b:8082"
],
"SplitBrainResolverStrategy": "keep-oldest",
"StableAfter": "00:00:15",
@@ -20,7 +20,7 @@
"MinNrOfMembers": 1
},
"Database": {
"SiteDbPath": "/app/data/scadalink.db"
"SiteDbPath": "/app/data/scadabridge.db"
},
"DataConnection": {
"ReconnectInterval": "00:00:05",
@@ -33,8 +33,8 @@
},
"Communication": {
"CentralContactPoints": [
"akka.tcp://scadalink@scadalink-env2-central-a:8081",
"akka.tcp://scadalink@scadalink-env2-central-b:8081"
"akka.tcp://scadabridge@scadabridge-env2-central-a:8081",
"akka.tcp://scadabridge@scadabridge-env2-central-b:8081"
],
"DeploymentTimeout": "00:02:00",
"LifecycleTimeout": "00:00:30",
+6 -6
View File
@@ -3,7 +3,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== ScadaLink Env2 Docker Teardown ==="
echo "=== ScadaBridge Env2 Docker Teardown ==="
echo "Stopping env2 application containers..."
docker compose -f "$SCRIPT_DIR/docker-compose.yml" down
@@ -12,8 +12,8 @@ echo ""
echo "Teardown complete."
echo "Site data (SQLite DBs) and logs are preserved in node directories."
echo ""
echo "Env2 databases (ScadaLinkConfig2 / ScadaLinkMachineData2) remain on"
echo "the shared scadalink-mssql. To drop them:"
echo " docker exec scadalink-mssql /opt/mssql-tools18/bin/sqlcmd \\"
echo " -S localhost -U sa -P 'ScadaLink_Dev1#' -C \\"
echo " -Q \"DROP DATABASE ScadaLinkConfig2; DROP DATABASE ScadaLinkMachineData2;\""
echo "Env2 databases (ScadaBridgeConfig2 / ScadaBridgeMachineData2) remain on"
echo "the shared scadabridge-mssql. To drop them:"
echo " docker exec scadabridge-mssql /opt/mssql-tools18/bin/sqlcmd \\"
echo " -S localhost -U sa -P 'ScadaBridge_Dev1#' -C \\"
echo " -Q \"DROP DATABASE ScadaBridgeConfig2; DROP DATABASE ScadaBridgeMachineData2;\""
+2 -2
View File
@@ -14,5 +14,5 @@ http:
interval: 5s
timeout: 3s
servers:
- url: "http://scadalink-env2-central-a:5000"
- url: "http://scadalink-env2-central-b:5000"
- url: "http://scadabridge-env2-central-a:5000"
- url: "http://scadabridge-env2-central-b:5000"