Commit a99e5558 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

docs(fm,srm): onboard apps at partners (ADR-0061), keep partner instance rows (ADR-0053)

ADR-0061: OPG.04 expects an application to be onboarded at the partner
(UploadFile, UploadArtefact, OnboardApplication) before InstallApp names
it. FM-A onboards lazily on the first InstallApp of a specification
version to a partner, with derived ids, public repositories and
container
units for now, and flavourId from the zone subscription. FM-B creates
the
specification in its SRM under the originating partner as tenant and
resolves InstallApp from it, replacing ADR-0018's manual app mapping.

ADR-0053 amended: SRM now writes a service_instance row per partner
target (hosted_by: partner), recording the partner zone as kind
federation_business so the zone foreign key holds, and completes the
order from event.fm.operation.completed. Without the rows an order with
more than one partner zone can't track which targets answered, and OEG
only updates instances listed in the final event. Terminates route to FM
on hosted_by instead of "not in SRM's inventory".
parent 97bbeff2
Loading
Loading
Loading
Loading
+15 −7
Changes for decisions/0053-srm-fm-federated-lifecycle-handoff-over-the-databus.md: 15 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@ status: "proposed"

# ADR-0053: SRM hands federated deploys and terminates to FM over the DataBus, and FM reports the outcome the same way

**Status:** proposed
**Status:** proposed (amended 2026-09-25: SRM keeps instance rows for partner targets)
**Date:** 2026-09-22
**Affects:** [fm/overview-and-architecture.md](/fm/overview-and-architecture.md) (open item G4, UC-FM-07), [fm/requirements-and-design.md](/fm/requirements-and-design.md) (the SRM→FM hand-off section §L.3a, FM's DataBus sections §K.2 and §M), [srm/interface-contract.md](/srm/interface-contract.md) (stream configuration §D, the federated hand-off flow §F.4a), [platform/architecture.md](/platform/architecture.md) (DataBus configuration)
**Amends:** ADR-0001 and ADR-0022, where they make SRM the only consumer of `OOP_TASKS`; ADR-0046, where it keeps the DataBus to `command.srm.*` / `event.srm.*`
@@ -119,7 +119,7 @@ isn't built for request/reply.
| Subject | Stream | Payload | SRM publishes it when |
|---|---|---|---|
| `command.fm.service.deploy` | `OOP_TASKS` | the `SrmServiceDeployV1` SRM received, unshaped | the target zone came back federated |
| `command.fm.service.terminate` | `OOP_TASKS` | the `SrmServiceTerminateV1` SRM received, unshaped | the `service_instance_id` isn't in SRM's inventory |
| `command.fm.service.terminate` | `OOP_TASKS` | the `SrmServiceTerminateV1` SRM received, unshaped | the instance's row says `hosted_by: partner` |

The envelope keeps the original `operation_id` and `correlation_id`. SRM does no GSMA shaping; FM
alone maps to `InstallApp` and `RemoveApp` (ADR-0044). Terminate has no lookup step: FM resolves the
@@ -192,11 +192,19 @@ written for the federated portion"), which splits in two:
  accepts. Its `UNIQUE(operation_id)` constraint is the idempotency anchor, and the row lets SRM
  match a late `event.fm.operation.completed` after a restart. The Component Boundary Matrix already
  gives service orders to SRM.
- **SRM still writes no `service_instance` row.** The instance runs on the partner's infrastructure,
  and SRM's inventory exists for placement, capacity queries and drift reconciliation; an instance
  SRM can't observe would show up as permanent drift. The schema couldn't hold it anyway:
  `service_instance.zone_id` is a non-null foreign key into SRM's own `zone` table, where a
  federated zone by definition isn't.
- **SRM writes a `service_instance` row for each partner target too**, in state `creating` with
  `metadata.hosted_by: partner`. An order can target zones at any number of partners, and SRM needs
  somewhere to remember which of them have answered; OEG also only updates an instance it finds in
  the final event's `instances[]`. Because `service_instance.zone_id` is a non-null foreign key into
  SRM's `zone` table, SRM records the partner zone there first, as `kind: federation_business`,
  from FM's `GET /internal/federated-zones/{zone_id}`. `list_resource_zones` only returns
  `kind: resource`, so these rows don't reach placement candidates or `GET /internal/zones` twice,
  and a later deploy to a recorded partner zone still goes to FM. Drift reconciliation must skip
  `hosted_by: partner` instances, since SRM can't observe them; it doesn't exist yet.
- **SRM completes the order from FM's answer.** The order stays `in_progress` while any target is
  `creating`. Each `event.fm.operation.completed` moves its instance to `active` or `failed`; once
  none is left `creating`, SRM completes the order (`completed`, `partially_completed` or `failed`)
  and publishes `event.srm.operation.completed` listing every instance.

FM keeps its `federation_transactions` row unchanged, as the audit and dispute record that ties the
partner's `appInstIdentifier` to our `operation_id`.
+117 −0
Changes for decisions/0061-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md: 117 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0061: FM onboards an application at a partner over the EWBI before the first InstallApp, and the partner resolves InstallApp from that onboarding"
description: "FM onboards an application at a partner over the EWBI before the first InstallApp, and the partner resolves InstallApp from that onboarding"
tags: [adr]
timestamp: 2026-09-25T00:00:00Z
status: "proposed"
---

# ADR-0061: FM onboards an application at a partner over the EWBI before the first `InstallApp`, and the partner resolves `InstallApp` from that onboarding

**Status:** proposed
**Date:** 2026-09-25
**Affects:** [fm/requirements-and-design.md](/fm/requirements-and-design.md) (outbound deploy path, inbound onboarding operations), [fm/persistence-model.md](/fm/persistence-model.md) (`shared_resource_catalogues` resource types, `federation_agreements.zone_resources`), [ADR-0018](0018-federation-tenancy-partner-as-tenant.md) (the agreement's app mapping), [ADR-0054](0054-fm-receives-gsma-callbacks-through-one-router.md) (one more callback kind)

## Context

GSMA OPG.04 v6.0 (artifact v1.4.0) expects an application to exist at the partner before the
originating OP asks for an instance of it. Its Application Onboarding Management API (PRD section
4.1.3) runs in three calls: `UploadFile` registers the container or VM images, `UploadArtefact`
registers the descriptor that uses them, and `OnboardApplication` registers the application under an
`appId` the originating OP picks. Only then does `InstallApp` name that `appId`, a version and a
`zoneInfo{zoneId, flavourId}`. The PRD prose talks about "already onboarded applications" and the
contract has no way to install something the partner has never seen.

FM does none of this today. As the partner, it resolves an inbound `InstallApp` through the
agreement's `app_mappings`: operator B types which `(appId, appVersion, flavourId)` means which of
its service specifications, and operator A has to use the same values. ADR-0018 chose that manual
mapping and gave as its revisit trigger "OPG.04 evolving a standard app-onboarding exchange over
EWBI". OPG.04 v6.0 already has that exchange, so the trigger had fired before the ADR was written.
The earlier R1 FM also onboarded applications over EWBI.

Two facts from the contract shape the mapping. `UploadFile` and `UploadArtefact` take
`multipart/form-data`, even when the file itself is only referenced by URL. And `appId` and
`appProviderId` must match `^[A-Za-z][A-Za-z0-9_]{7,63}$`, which rules out a UUID with hyphens.

## Decision

**FM-A onboards lazily, on the first `InstallApp` of a service specification to a given partner.**
When FM-A is asked to deploy a specification on a partner's zone and has no onboarding of that
specification version at that partner yet, it:

1. reads the specification from SRM-A (`GET /internal/catalog/service-specifications/{id}`), since
   the deploy request carries only its id;
2. sends one `UploadFile` per container image, by reference: `repoType: PUBLICREPO`,
   `fileRepoLocation` set to the deployment unit's `artifact_ref`, `fileType: DOCKER`;
3. sends one `UploadArtefact` per deployment unit, descriptor type `COMPONENTSPEC`, with
   `componentSpec.images` naming the file ids from step 2 and the unit's CPU and memory as its
   compute profile;
4. sends `OnboardApplication` with `appComponentSpecs` naming the artefacts, `appMetaData`
   (name, version, a generated `accessToken`), `appQoSProfile.latencyConstraints: NONE` (SRM's
   specification has no equivalent) and an `appStatusCallbackLink`;
5. records the onboarding and then sends `InstallApp`.

A later deploy of the same specification version to the same partner goes straight to step 5.

**Identifiers are derived, not stored and looked up.** `appId` is `oop_` plus the specification
id in hex, `appProviderId` is the specification's `app_provider_id` (our App Provider, the tenant at A) rewritten into GSMA's pattern, and
`fileId` and `artefactId` are UUIDs derived from the specification and deployment unit. A retried
onboarding therefore sends the same ids, and a `409` from the partner means "already there" and
is treated as success.

**Only public repositories in this release.** A deployment unit whose image sits in a private
repository is refused before anything is sent. GSMA would carry the pull credential inside
`fileRepoLocation`, but OEG does not keep the App Provider's credential today (it stores only a
`secret://` reference with no store behind it), so there is nothing to send. Container units come
first; Helm units need image references SRM does not record yet.

**`zoneInfo.flavourId` comes from the zone subscription.** FM-A stores the `flavoursSupported` a
partner returned on `ZoneSubscribe` and uses one of them. A zone FM-A has not subscribed to is not
deployable, which matches OPG.04's split between offered and accepted zones.

**FM-B turns an onboarding into a specification in SRM-B.** On `OnboardApplication` it creates a
service specification in SRM-B (`POST /internal/catalog/service-specifications`) under the
partner as tenant (ADR-0018 option a still holds: `app_provider_id` is the partner), from the
files and artefacts it stored, and records `(partner, appId, version)` against the new
specification id. It answers `200` because SRM's catalogue write is synchronous. An inbound
`InstallApp` is then resolved from that record instead of the agreement's `app_mappings`, and the
agreement keeps deciding whether the partner may deploy at all and where. This replaces the
manual mapping part of ADR-0018.

**A partner that answers `202` gets a callback.** GSMA lets `OnboardApplication` (and the two
uploads) answer `202` and report later on `appStatusCallbackLink`. FM-A waits for that callback
(`onApplicationOnboardStatusEvent`, a new kind under ADR-0054's router) before `InstallApp`.

**Two ways into the same outbound pipeline.** SRM-A's federated hand-off (ADR-0053) is the
product path. FM also takes the same request from the operator on its internal surface, so the
exchange between two FMs can be run and tested without the SRM hand-off.

**Where it is stored.** No new tables: `shared_resource_catalogues` gains resource types, the
same way zone subscriptions were added. FM-A keeps `app_onboarding` rows (outbound: partner,
specification, version, `appId`, state). FM-B keeps `file`, `artefact` and `application` rows
(inbound: what the partner uploaded and which specification the application became).

## Consequences

- An inbound `InstallApp` no longer depends on two operators typing matching values; the partner
  learns the application from the originating OP itself.
- The first deploy of an application to a partner costs three to five extra EWBI calls; later
  deploys cost none.
- Deleting a specification in SRM-A does not yet deboard it at partners (`DeleteApp` is not
  called). That is left for a later change.
- Private-repository images and Helm units are refused for now, with an error that says so.
- The agreement's `app_mappings` stops being read for `InstallApp`. It can be removed once no
  deployment relies on it.

## Alternatives considered

- **Keep the agreement's `app_mappings`.** Works today and costs nothing, but skips a step GSMA
  expects, needs both operators to agree on values by hand, and gives the partner no image or
  descriptor to deploy from except what it onboarded itself.
- **Onboard eagerly, when the App Provider registers the application.** Pushes every application
  to every partner whether or not it is ever deployed there, and needs a policy for which partners
  get which applications. Lazy onboarding only reaches partners that are actually used.
- **Upload the artefact bytes (`repoType: UPLOAD`).** Would need FM to fetch and hold images or
  charts. Referencing the public repository leaves the partner to pull them, which is what
  `PUBLICREPO` exists for.
+2 −1
Changes for decisions/index.md: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -52,5 +52,6 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0049](0049-terminal-record-read-follows-camara-schema.md) - terminal-record read behaviour follows each family's own vendored CAMARA schema: Traffic Influence returns 200 with `state: deleted`, QoD stays 404 (amends ADR-0034's blanket 404 rule and its 204 delete code)
* [ADR-0050](0050-oeg-federation-lifecycle-proxy-errors-are-gsma-problem-json.md) - OEG's federation-lifecycle proxy endpoints (§N.7, ADR-0046) return OPG.04 `application/problem+json` `ProblemDetails` for every error - FM's errors relayed unchanged, OEG's own 400/401/403/502/503 from a fixed template; ADR-0030's `ErrorInfo` rule is scoped to OEG's CAMARA northbound surface
* [ADR-0051](0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md) - OEG fronts eight GSMA federation-lifecycle operations on its own platform namespace, `/platform/v1/federation/partners/{partnerOpId}/federations/...`, scoped by a registered `partnerOpId` (create/read/update/delete federation, fed-context-id lookup, zone list/subscribe/details/unsubscribe), forwarding the raw GSMA request/response byte-for-byte to FM including the query string, pinned to GSMA OPG.04 v6.0 / artifact v1.4.0; operator-triggered outbound only, separated from inbound EWBI at the ingress; keeps ADR-0046's sync-HTTP/stateless/no-DataBus transport and ADR-0050's error model (amends this ADR's own first version, which used the bare canonical OPG.04 paths and lost the partner addressing GSMA gets for free from the URL)
* [ADR-0053](0053-srm-fm-federated-lifecycle-handoff-over-the-databus.md) - SRM hands federated deploys and terminates to FM as `command.fm.service.deploy` / `.terminate` on `OOP_TASKS`, and FM reports the outcome as `event.fm.operation.completed` on `OOP_EVENTS`; zone lookups stay synchronous HTTP reads. `completion_basis` separates a confirmed deploy from a terminate the partner only accepted. SRM writes a `service_order` row for federated work but no `service_instance` row; FM waits 30 minutes by default for a deploy callback. Amends ADR-0001/0022 (FM consumes `command.fm.>`) and ADR-0046's DataBus scope; resolves G4
* [ADR-0053](0053-srm-fm-federated-lifecycle-handoff-over-the-databus.md) - SRM hands federated deploys and terminates to FM as `command.fm.service.deploy` / `.terminate` on `OOP_TASKS`, and FM reports the outcome as `event.fm.operation.completed` on `OOP_EVENTS`; zone lookups stay synchronous HTTP reads. `completion_basis` separates a confirmed deploy from a terminate the partner only accepted. SRM writes a `service_order` row and a `service_instance` row per partner target (`hosted_by: partner`, partner zone recorded as `federation_business`) and completes the order from FM's answer; FM waits 30 minutes by default for a deploy callback. Amends ADR-0001/0022 (FM consumes `command.fm.>`) and ADR-0046's DataBus scope; resolves G4
* [ADR-0054](0054-fm-receives-gsma-callbacks-through-one-router.md) - FM receives every OPG.04 callback on `/operatorplatform/federation/v1/callbacks/{kind}`: one router that checks the `fed-mgmt-notif` scope and that `federationContextId` is an outbound context of the calling partner, then a per-kind idempotent handler. `onPartnerStatusEvent` first; `FM_PARTNER_STATUS_LINK` moves to `/callbacks/partner-status`. The `federation` realm gets `fed-mgmt-notif` as an optional scope on partner clients
* [ADR-0061](0061-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md) - FM onboards an application at a partner over the EWBI (`UploadFile`, `UploadArtefact`, `OnboardApplication`) on the first `InstallApp` of that specification version, with derived GSMA ids, public repositories and container units only for now, and `flavourId` from the zone subscription; the partner creates the specification in its SRM under the originating partner as tenant and resolves `InstallApp` from that onboarding, replacing ADR-0018's manual app mapping. Adds `onApplicationOnboardStatusEvent` to ADR-0054's router
+6 −5
Changes for srm/interface-contract.md: 6 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -899,11 +899,12 @@ instance unknown) arrives as the same event with `status: failed` and the reason
**Record ownership (amended by ADR-0053).** SRM **writes a `service_order` row** for federated
deploys and terminates, as for every command it accepts. It's the `UNIQUE(operation_id)` idempotency
anchor (§D.4) and what lets SRM match a late `event.fm.operation.completed` after a restart. SRM
**writes no `service_instance` rows**: the instance runs on the partner's infrastructure, SRM's
inventory exists for placement, capacity queries and drift reconciliation, and an instance SRM
can't observe would show up as permanent drift. (`service_instance.zone_id` is also a non-null
foreign key into SRM's `zone` table, where a federated zone isn't.) OEG keeps its own `app_instances`
rows and answers App Provider reads from `oeg_db`, so nothing upstream needs an SRM instance row.
also **writes a `service_instance` row per partner target**, state `creating`, `metadata.hosted_by:
partner`, after recording the partner zone in `zone` as `kind: federation_business` (the row's
`zone_id` is a foreign key). The order stays `in_progress` until every partner target has an
`event.fm.operation.completed`; SRM then completes it and publishes `event.srm.operation.completed`
listing every instance, which is what OEG updates its `app_instances` from. Drift reconciliation
skips `hosted_by: partner` instances.

**Retry ownership.** SRM owns getting the command onto `OOP_TASKS`: it publishes through JetStream
and waits for the acknowledgement, inside its own `command.srm.*` handling. FM owns everything after