ADR 007: Central Orchestrator Function & Crossplane v2-Only¶
Status¶
Accepted. Supersedes ADR 006: Multi-Provider Compositions.
Context¶
Hiroba is being reshaped around a small set of agnostic, reusable primitives
(e.g. PostgresInstance, ObjectBucket) that any stack may compose, each with an
inherent backend toggle (spec.provider: aws, gcp, azure, garage, cnpg,
s3, local). Nothing in the catalog has been published as a product yet, so we are
free to remove code rather than migrate it.
ADR 006 implemented the provider toggle as one Composition per backend
(compositions/aws.ts, compositions/cnpg.ts, ...) selected by spec.provider via
compositionSelector. That approach:
- duplicates Composition scaffolding per backend;
- cannot cleanly compose a primitive inside a stack (patch-and-transform does not orchestrate child XRs well);
- still embeds provider branching in generated YAML rather than in testable code.
We also decided to standardize on Crossplane v2 exclusively: namespaced composite resources (no legacy Claim model), v2 XRDs, and (optionally) namespaced providers for per-team credential isolation.
Decision¶
- Single central orchestrator Composition Function (
function-platform), written in Go withfunction-sdk-go. Every Composition — primitives and stacks — runs inPipelinemode with one step that callsfunction-platform. The function dispatches on the observed composite'sapiVersion/kindvia an internal registry and emits the desired composed resources for that kind. - Provider branching lives in the function, not in per-backend Composition files.
A primitive handler reads
spec.providerand emits the matching managed resource(s) (e.g. RDSInstanceforaws, CNPGClusterforcnpg). - Hierarchical composition: a stack handler emits child primitive XRs as composed
resources; those child XRs are reconciled by their own Composition (also calling
function-platform), which emits the managed resources. One binary orchestrates the whole stack → primitive → managed tree. - Crossplane v2 only. All XRDs use
apiextensions.crossplane.io/v2withspec.scope: Namespaced; the legacyclaimNames/ cluster-scoped Claim model is not used. Consumers create the XR directly in their namespace. - Namespaced providers (
pkg.m.crossplane.io) for per-team credential isolation. The function resolvesspec.providerConfigRefto a namespace-scopedProviderConfig(convention<team>-<provider>, overridable on the XR). - A stable connection-secret contract is defined per primitive (see
packages/shared) so stacks can later reference a primitive's connection details instead of inlining infrastructure.
Consequences¶
- One Composition per product (not per backend); adding a backend is a branch in a handler, not a new Composition file.
- Shared concerns (provider → region /
providerConfigRefmapping, mandatory labels, naming, connection-secret aggregation) live once in the function. - Introduces a Go toolchain into a previously TypeScript-only repository:
go.mod, Dockerfile, function-image/xpkg build and a CI lane. - ADR 006's per-provider Composition file layout is retired.
- All existing v1 XRDs and
*Claimusage are removed/rewritten as part of this change.
v2-only / delete checklist (completed 2026-07-12)¶
- [x] All XRDs emit
apiextensions.crossplane.io/v2withspec.scope: Namespaced. - [x] No
claimNameson any XRD; no*Claimkinds emitted by the consumer SDK. - [x] CI lint gate rejects
apiextensions.crossplane.io/v1XRDs and anyclaimNames. - [x] Remove per-provider Composition file split (
compositions/{aws,cnpg,garage,...}.ts). - [x] Remove inline RDS/CNPG from
grafanaand inline S3/Garage fromloki/mimir; replaced by childPostgresInstance/ObjectBucketXRs emitted by the handlers. - [x] Remove dead
functions/grafana-sso/*remnants. - [x] Remove deprecated
CloudProvider/MultiCloudResourceConfigaliases inpackages/shared/src/types.ts. - [x] Collapse the duplicated
packages/sharedin the Observability Stack repo; consume Hiroba's@7k-hiroba/sharedas the single source of truth.
v2 conformance notes (added 2026-07-12, live e2e on kind)¶
- Crossplane v2 drops XR-level connection secrets:
connectionSecretKeysis not supported on v2 XRDs, and function-returned connection details for child XRs are not persisted. The contract surface isstatus(endpoint, phase, connectionSecretRef); cross-child wiring reads the child XR'sstatus, not connection details. - The v2 XRD -> CRD converter rejects
additionalPropertiescombined withpropertieson nested object fields; keep nested schemas structural or free-form. - Namespaced provider APIs (
*.m.*, e.g.helm.m.crossplane.io/v1beta1,*.m.upbound.io/v1beta1) requirespec.providerConfigRef.kind; legacy cluster-scoped APIs forbid it.SetProviderConfigRefbranches on the composed resource's apiVersion. - Function pipeline readiness is explicit: composed resources without a readiness
verdict are treated as unready. Handlers set readiness from the observed
resource's
Readycondition or a healthystatus.phase. - The function container reads mTLS certs from
/tls/server(Crossplane v2 mount).