Refactor/fm
Summary
Implements OEG's side of the 7 operator-triggered outbound federation-lifecycle operations (ADR-0046 / ADR-0047 / ADR-0048) as a stateless, synchronous HTTP proxy to FM:
POST /partner, GET/DELETE /{federationContextId}/partner, GET /fed-context-id, POST /{federationContextId}/zones, GET/DELETE /{federationContextId}/zones/{zoneId}.
OEG forwards the raw request body to FM byte-for-byte and relays FM's response (status, body, content-type) back unaltered. The two POST bodies (POST /partner, POST /{federationContextId}/zones) are validated for shape against the GSMA schema before being forwarded — a request missing required fields is rejected with 400 and FM is never called — but the validated object is only a gate: what's actually sent to FM is always the caller's original bytes, not a rebuilt payload, so a field outside OEG's model still reaches FM unchanged. FM's response is never parsed against a schema, only checked for basic JSON well-formedness (→ 502 if unusable), and FM being unreachable → 503.
Needs your confirmation: FM-side internal endpoint paths
fm/requirements-and-design.md §L.3b says FM exposes "matching internal endpoints... under /internal/federation/" but doesn't name the per-operation paths.
In the absence of that table, I implemented OEG's client (fm_client.py) to mirror the operator-facing path 1:1 under that prefix. This is currently an assumption, not a confirmed contract — please confirm or correct it before this merges:
POST /partner->POST /internal/federation/partnerGET /{federationContextId}/partner->GET /internal/federation/{federationContextId}/partnerDELETE /{federationContextId}/partner->DELETE /internal/federation/{federationContextId}/partnerGET /fed-context-id->GET /internal/federation/fed-context-idPOST /{federationContextId}/zones->POST /internal/federation/{federationContextId}/zonesGET /{federationContextId}/zones/{zoneId}->GET /internal/federation/{federationContextId}/zones/{zoneId}DELETE /{federationContextId}/zones/{zoneId}->DELETE /internal/federation/{federationContextId}/zones/{zoneId}
Please also confirm this /internal/federation/* prefix is a separate surface from FM's public /operatorplatform/federation/v1/* EWBI path. Per ADR-0046 the two are meant to be distinct — different auth (platform service credential vs. partner fed-mgmt token) and /internal/* is never exposed through Nginx/Ingress. If FM's actual implementation instead reuses the public EWBI path for this, fm_client.py's base path needs to change to match.
Other things FM's side needs to support, per the ADRs
- Auth: OEG will eventually present a platform service credential (OAuth2 client-credentials, ADR-0042) on this call. Not implemented yet on OEG's side — this MR ships with nothing attached on the outbound call, so FM shouldn't hard-gate on it until that lands too.
- Byte-for-byte on the body FM actually receives: OEG only checks shape on the two POST bodies as a pre-flight gate; the bytes FM receives are always the operator's original payload, never something OEG rebuilt.
- Errors: FM's error responses should be
application/problem+jsonProblemDetails(ADR-0047) — OEG relays whatever FM sends unaltered. - Secrets:
FederationRequestData.partnerCallbackCredentials.clientSecretpasses through this call. OEG doesn't log or persist request/response bodies on this path — please confirm FM doesn't either, beyond what §S.4 requires.
Out of scope for this MR
update_federation(PATCH /{federationContextId}/partner) — intentionally excluded per current requirements.- Operator-role auth gate and the OEG→FM platform service credential itself.
- Reconciling
federation-manager.yaml(currently v1.2.0, ~20 ops) against the "OPG.04 v6.0 / OpenAPI v1.4.0 / 74 operations" language in FM's own docs (ADR-0048 open item).
Test plan
Automated (in this MR)
-
tests/unit/test_fm_client.py—FmClient.relaytransport behavior (method/URL/body/headers forwarded correctly; FM error responses passed through untouched; timeout/connection failure raiseFmUnavailableError) -
tests/unit/test_federation_manager_endpoints.py— router-level relay contract, via a fakeFmClient(no real network):- All 7 endpoints forward the correct method/path/body/
X-Correlatorto FM and relay a successful FM response byte-for-byte - FM error response (e.g.
409) relayed unaltered, status/body/content-type preserved - FM unreachable →
503ProblemDetails - Malformed (non-JSON) request body →
400, FM never called - Well-formed JSON but wrong shape (
POST /partner,POST /{federationContextId}/zones) →400, FM never called - Valid-shape request body is still forwarded as the original raw bytes (validation is a gate, not a reshape)
- Unparseable FM response body →
502 - OpenAPI exposes all 7
operationIds
- All 7 endpoints forward the correct method/path/body/
-
ruff checkclean -
mypyclean (71 source files) - Full unit suite passes (580 tests)
Manual / pending FM's side
- End-to-end call against a real FM instance once
/internal/federation/*exists, to confirm the path mapping listed above - Confirm FM returns
application/problem+jsonProblemDetailsfor its own errors, per ADR-0047 - Confirm FM doesn't log/persist
partnerCallbackCredentials.clientSecretfrom the request/response body