Files
ScadaBridge/docker/seed-flow-outbound-demo.sh
T
Joseph Doherty e2f433e849 feat(cli): read secret-bearing option values from a file or stdin (#137)
Every CLI option that can carry a secret gets a `--<name>-file <path|->`
sibling ('-' reads stdin), so the value stays out of the process list and
shell history: --password-file (global), --primary-config-file /
--backup-config-file (data-connection create|update), --auth-config-file
(external-system create|update), --connection-string-file (db-connection
create|update), --credentials-file (notification smtp create|update) and
--auth-token-file (notification sms create|update). Follows the existing
--config-file convention; `@path` is avoided because System.CommandLine
expands @-tokens as response files.

The value is read at parse time; a missing/unreadable/empty source, or '-'
on a terminal stdin, is a parse error naming option + path only, never the
content. Inline and file forms are mutually exclusive, at most one option
may read stdin, and one trailing line break is stripped. Inline forms keep
working.

docker/seed-sites.sh and docker-env2/seed-sites.sh now write the MxGateway
config (windev gateway key) to a mode-600 temp file removed by an EXIT trap
and pass --primary-config-file; seed-flow-outbound-demo.sh pipes its rig
connection string via --connection-string-file -. CLI README (new "Secrets
from a file or stdin" section + per-command tables), Component-CLI.md and
both docker READMEs updated.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-13 00:46:17 -05:00

108 lines
7.0 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Seed the Flow Debugger OUTBOUND-CALLS demo on the docker rig (plan
# docs/plans/2026-09-06-flow-debugger-outbound-calls.md, Task 10): one site Call script that makes
# all three outbound call kinds, plus an inbound method routing to it, so `flow-debug run-method` /
# the /deployment/flow-debugger page show ApiCall + DbCall + Notify nodes on the External lane.
#
# What it creates:
# - External system RigRest -> the infra REST API (scadabridge-restapi:5200, X-API-Key
# scadabridge-test-key-1) with one method Echo = POST /api/Echo.
# - DB connection RigDb -> the rig's ScadaBridgeMachineData database on scadabridge-mssql.
# - Notification list FlowDemo (email; delivery needs an SMTP configuration row — create one on
# /notifications/smtp pointing at scadabridge-smtp:1025, auth None —
# otherwise the notification node ends Parked, which is itself a
# valid terminal lifecycle for the demo).
# - Script OutboundDemo (Call) on template SimMachine (docker/seed-delmia-sim.sh):
# ExternalSystem.Call("RigRest","Echo") -> sync ApiCall, in-chain
# Database.CachedWrite("RigDb", …) -> DbCall, async (creates + inserts dbo.FlowDemo)
# Notify.To("FlowDemo").Send(…) -> Notify, async
# - Inbound method FlowOutboundDemo (Route.To(MachineCode).Call("OutboundDemo")) + an API key
# scoped to it (token printed once as FLOW_DEMO_API_KEY=…).
# - Artifacts pushed to site 1 and DelmiaSim01 redeployed so the site has the new script.
#
# Not idempotent (names must be unique). Prerequisites: docker/deploy.sh, seed-sites.sh,
# seed-delmia-sim.sh, and `infra/` up (restapi + mssql).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CLI="$PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge"
A=(--url "${SCADABRIDGE_URL:-http://localhost:9000}" --format=json --username "${SCADABRIDGE_USERNAME:-multi-role}" --password "${SCADABRIDGE_PASSWORD:-password}")
SITE_ID="${SITE_ID:-1}"
INSTANCE_NAME="${INSTANCE_NAME:-DelmiaSim01}"
TEMPLATE_NAME="${TEMPLATE_NAME:-SimMachine}"
KEY_NAME="${KEY_NAME:-flow-outbound-demo}"
RIG_DB_CS="${RIG_DB_CS:-Server=scadabridge-mssql,1433;Database=ScadaBridgeMachineData;User Id=scadabridge_app;Password=ScadaBridge_Dev1#;TrustServerCertificate=true}"
json_id() { python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'; }
find_id() { # $1 = json array on stdin key name, $2 = value
python3 -c 'import json,sys; k,v=sys.argv[1],sys.argv[2]; print(next(x["id"] for x in json.load(sys.stdin) if x.get(k)==v))' "$1" "$2"; }
echo "1/7 External system RigRest -> scadabridge-restapi (method Echo = POST /api/Echo)..."
ES_ID=$("$CLI" external-system create "${A[@]}" --name RigRest --endpoint-url http://scadabridge-restapi:5200 \
--auth-type ApiKey --auth-config '{"key":"scadabridge-test-key-1"}' --timeout-seconds 10 | json_id)
"$CLI" external-system method create "${A[@]}" --external-system-id "$ES_ID" --name Echo --http-method POST --path /api/Echo > /dev/null
echo " id=$ES_ID"
echo "2/7 DB connection RigDb -> scadabridge-mssql/ScadaBridgeMachineData..."
# The connection string carries a password: pipe it in (--connection-string-file -) so it is
# not in the CLI's argv (ScadaBridge#137). printf is a builtin, so it is not in any argv.
DB_ID=$(printf '%s' "$RIG_DB_CS" | "$CLI" db-connection create "${A[@]}" --name RigDb --connection-string-file - | json_id)
echo " id=$DB_ID"
echo "3/7 Notification list FlowDemo..."
LIST_ID=$("$CLI" notification create "${A[@]}" --name FlowDemo --emails flow-demo@rig.local | json_id)
echo " id=$LIST_ID"
echo "4/7 Script OutboundDemo (Call) on template $TEMPLATE_NAME..."
TEMPLATE_ID=$("$CLI" template list "${A[@]}" | find_id name "$TEMPLATE_NAME")
read -r -d '' DEMO_CODE <<'CS' || true
// Flow Debugger outbound-calls demo: one sync API call (in-chain), one cached DB write and one
// notification (both async branches). Each is an ordinary audited call — nothing here is
// debugger-specific; the Flow Debugger just draws the rows they leave.
System.Func<string,object?> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
var wo = P("WorkOrder")?.ToString() ?? "WO-0";
var echo = await ExternalSystem.Call("RigRest", "Echo",
new System.Collections.Generic.Dictionary<string, object?> { ["workOrder"] = wo, ["source"] = "OutboundDemo" });
var db = await Database.CachedWrite("RigDb",
"IF OBJECT_ID('dbo.FlowDemo','U') IS NULL CREATE TABLE dbo.FlowDemo (Id INT IDENTITY(1,1) PRIMARY KEY, WorkOrder NVARCHAR(64) NOT NULL, At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()); INSERT INTO dbo.FlowDemo (WorkOrder) VALUES (@wo);",
new System.Collections.Generic.Dictionary<string, object?> { ["wo"] = wo });
var notification = await Notify.To("FlowDemo").Send("Flow demo " + wo, "Echo ok=" + echo.Success + "; db op=" + db.Value);
return new { Result = echo.Success, ResultText = "echo=" + echo.Success + " db=" + db.Value + " notify=" + notification };
CS
"$CLI" template script add "${A[@]}" --template-id "$TEMPLATE_ID" --name OutboundDemo --trigger-type Call --code "$DEMO_CODE" > /dev/null
echo " template id=$TEMPLATE_ID"
echo "5/7 Inbound method FlowOutboundDemo + API key '$KEY_NAME'..."
read -r -d '' METHOD_CODE <<'CS' || true
System.Func<string, object> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
var code = (P("MachineCode") as string ?? "").Trim();
if (string.IsNullOrEmpty(code)) return new { Result = false, ResultText = "MachineCode is required" };
return await Route.To(code).Call("OutboundDemo", new { WorkOrder = P("WorkOrder") });
CS
METHOD_ID=$("$CLI" api-method create "${A[@]}" --name FlowOutboundDemo --timeout 45 \
--parameters '{"type":"object","properties":{"MachineCode":{"type":"string"},"WorkOrder":{"type":"string"}},"required":["MachineCode"]}' \
--return-def '{"type":"object","properties":{"Result":{"type":"boolean"},"ResultText":{"type":"string"}},"required":["Result","ResultText"]}' \
--script "$METHOD_CODE" | json_id)
KEY_JSON=$("$CLI" security api-key create "${A[@]}" --name "$KEY_NAME" --methods FlowOutboundDemo)
TOKEN=$(echo "$KEY_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])' 2>/dev/null || echo "$KEY_JSON" | grep -o 'sbk_[A-Za-z0-9_-]*' | head -1)
echo " method id=$METHOD_ID"
echo "6/7 Pushing artifacts (external system + DB connection) to site $SITE_ID..."
"$CLI" site deploy-artifacts "${A[@]}" --site-id "$SITE_ID" > /dev/null
echo "7/7 Redeploying $INSTANCE_NAME so the site has OutboundDemo..."
INSTANCE_ID=$("$CLI" instance list "${A[@]}" | find_id uniqueName "$INSTANCE_NAME")
"$CLI" instance deploy "${A[@]}" --id "$INSTANCE_ID" > /dev/null
echo " instance id=$INSTANCE_ID"
echo
echo "FLOW_DEMO_API_KEY=$TOKEN"
echo "Try: scadabridge --url http://localhost:9000 flow-debug run-method --method-id $METHOD_ID --params '{\"MachineCode\":\"$INSTANCE_NAME\",\"WorkOrder\":\"WO-1\"}'"
echo " then open /deployment/flow-debugger?executionId=<rootExecutionId>"