openova/infra
e3mrah 22855e62d8
feat(openova-flow): catalyst-api proxy + cloud-init thread (Agent #3 — integrator, infra-side) (#1396)
Final integration piece for OpenovaFlow infrastructure path —
catalyst-api proxy + cloud-init substitution for SOVEREIGN_DEPLOYMENT_ID
+ SOVEREIGN_REGION_KEY, so bp-openova-flow-emitter (slot 57) emits
distinct region tags on every FlowNode and the snapshot returns 2× per
HR on a multi-region Sovereign.

Builds on PR #1389 (TS core + canvas packages on disk), PR #1390 (Go
server + flux adapter + bootstrap-kit slots 56/57), PR #1394 (catalyst-
ui temporary revert until npm workspaces land), PR #1395 (chart no-op).

## Scope vs original Agent #3 brief

The brief planned a 4-section PR (proxy + cloud-init + FlowPage rewire +
runbook). Section 3 (catalyst-ui rewire of @openova/flow-*) is deferred:
PR #1394 reverted Agent #1's UI wiring because the Docker UI build has
no node_modules for the cross-workspace canvas source. Founder note on
#1394: "Agent #3 (or a follow-up) will re-wire them properly once npm
workspaces are configured at repo root."

This PR ships the infrastructure half (proxy + cloud-init + runbook).
The canvas-side rewire is a separate follow-up PR that needs npm
workspaces, not surgical edits to FlowPage.

## What ships

### 1. catalyst-api proxy /api/v1/flows/{deploymentId}/{snapshot,stream,events}

products/catalyst/bootstrap/api/internal/handler/openova_flow_proxy.go:
- GET /snapshot — JSON pass-through, headers + status forwarded
- GET /stream — unbuffered SSE pass-through using http.Flusher (NOT
  httputil.ReverseProxy; that buffers and breaks text/event-stream)
- POST /events — body forwarded byte-for-byte
- Upstream URL from env OPENOVA_FLOW_SERVER_URL (default Sovereign
  in-cluster Service DNS)

Routes registered in cmd/api/main.go inside the auth-gated chi.Group.

11 table-driven tests cover snapshot/events/stream pass-through, upstream
404/400/unreachable propagation, empty-deploymentId guard, SSE frames
arrive AS EMITTED, and env-default fallback.

### 2. Cloud-init threads SOVEREIGN_DEPLOYMENT_ID + SOVEREIGN_REGION_KEY

- infra/hetzner/cloudinit-control-plane.tftpl — two new postBuild.
  substitute keys alongside SOVEREIGN_FQDN/SOVEREIGN_LB_IP
- infra/hetzner/main.tf — primary CP renders var.region as region key;
  secondary CP renders each.key (e.g. "hel1-1") from for_each over
  local.secondary_regions
- infra/hetzner/variables.tf — new sovereign_deployment_id var (string,
  default "" for tofu mocks)
- provisioner.go writeTfvars — writes vars["sovereign_deployment_id"]
  = req.DeploymentID
- bootstrap-kit slot 57 — swap placeholder ${SOVEREIGN_FQDN} / literal
  "primary" for the new ${SOVEREIGN_DEPLOYMENT_ID} / ${SOVEREIGN_REGION_KEY}
  envsubst keys

### 3. Deployment record flag

handler/deployments.go State() — emits `openovaFlowEnabled: true` on
every deployment. The catalyst-ui rewire (follow-up PR) will read this
to enable the openova-flow-server adapter; legacy provisions without
the flag will keep the bridge once the rewire lands.

### 4. Verification runbook

docs/runbooks/openova-flow-multi-region-verify.md — prov #34 POST body
(multi-region cpx42 fsn1+hel1, qaTestEnabled=true,
sovereignFQDN=omantel.biz), step-by-step kubectl/curl gates, visual
canvas checks (gated on the follow-up UI rewire), and a failure-class
triage table.

## Canonical-seam citations

1. SSE pattern — products/catalyst/bootstrap/api/internal/handler/
   deployments.go:1244-1287 (StreamLogs): identical Content-Type +
   Cache-Control + X-Accel-Buffering header set; identical
   http.Flusher.Flush() after each write; identical r.Context().Done()
   cancel path.

2. postBuild.substitute pattern — infra/hetzner/cloudinit-control-plane.tftpl:884-893
   (SOVEREIGN_FQDN + SOVEREIGN_LB_IP): same indentation, same KEY: ${var}
   form, dual emission at primary + secondary CP for_each in main.tf.

## Verification

```
$ go build ./...
(clean)

$ go vet ./...
(clean)

$ go test ./internal/handler/ -run TestFlowProxy -count=1 -race
ok    github.com/openova-io/openova/products/catalyst/bootstrap/api/internal/handler   1.410s

$ go test ./internal/provisioner/... -count=1
ok    github.com/openova-io/openova/products/catalyst/bootstrap/api/internal/provisioner  0.025s
```

3 pre-existing test failures (TestHandleWhoami_NoRBACOmitsFields,
TestHandleWhoami_PinSessionRBACClaims,
TestUnstructuredToUserAccess_NilApplicationsBecomesEmpty) reproduce on
main HEAD without this PR — unrelated baseline state.

Co-authored-by: hatiyildiz <269457768+hatiyildiz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:01:09 +04:00
..
cloudflare-worker-leases feat(continuum): K-Cont-4 — Cloudflare Worker source + tofu wiring for lease witness (#1101) (#1159) 2026-05-09 08:01:44 +04:00
hetzner feat(openova-flow): catalyst-api proxy + cloud-init thread (Agent #3 — integrator, infra-side) (#1396) 2026-05-11 16:01:09 +04:00