composition

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package composition provides the public Composition Root abstraction for GoCell assemblies.

Overview

A Composition Root is the single place in an application where all objects are assembled together. Before this package existed, the assembly logic was trapped inside cmd/corebundle (package main, not importable from tests or alternative entry points).

Package composition exposes three public types:

  • CellModule — the per-Cell wiring contract. Each Cell declares itself to Builder.Build via one CellModule implementation.
  • SharedDeps — cross-cutting dependencies shared by every CellModule. Contains Clock, JWT, metrics, event bus and capability providers.
  • Builder / App — assembly entry point and runner.

Layering constraint

runtime/composition is governed by the runtime-isolation depguard rule in .golangci.yml: it may import only stdlib, kernel/…, pkg/…, and the curated runtime/… + external allowlist. In particular:

  • ZERO imports of adapters/…
  • ZERO imports of github.com/prometheus/client_golang

If the caller needs an adapter-specific type (e.g. *promadapter.MetricProvider), it must be assigned to the appropriate kernel/runtime interface declared in SharedDeps (MetricsProvider kernel/observability/metrics.Provider).

AUTH-PLAN-04 constraint

runtime/composition is forbidden from constructing auth plans (auth.NewAuthJWT / auth.NewAuthServiceToken / etc.). Listener and auth wiring must be supplied by the caller via the RuntimeOptionsFunc passed to Builder.Build. The composition root (cmd/…) owns that responsibility.

See RuntimeOptionsFunc and examples/corebundlestarter for a runnable example of supplying listener/auth wiring.

ref: uber-go/fx fx.App — single assembly entry point. ref: kubernetes-sigs/controller-runtime pkg/manager — Manager pattern.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the assembled, ready-to-run GoCell application. It is returned by Builder.Build and started by calling App.Run.

App is intentionally thin: it delegates directly to bootstrap.New(...).Run, keeping all assembly logic in Builder.Build.

func (*App) Run

func (a *App) Run(ctx context.Context) error

Run starts the bootstrap lifecycle and blocks until the context is canceled or a fatal error occurs.

ref: kubernetes-sigs/controller-runtime pkg/manager/internal.go — Manager.Start(ctx) error.

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder assembles a GoCell application from an ordered set of [CellModule]s.

ref: uber-go/fx fx.New(opts...) — single assembly entry point used by both production (main) and tests. ref: kubernetes-sigs/controller-runtime pkg/manager/manager.go — Manager accumulates options via functional options.

func New

func New(expectedCellIDs ...string) *Builder

New returns a new Builder whose composed cells must form exactly the assembly's declared cell-id closed set (expectedCellIDs — the assembly.yaml cells list, threaded in by the generated entrypoint). Build fail-fasts if any composed module's ID is not in the set, if a declared cell is not provided by any module, or on duplicate module IDs (see Build).

This is the M12a build-time closed-set guard (#1093): once multi-module assembly composition exists (#1086), the assembly.yaml cell set is the authoritative enumeration of legal cell identities, and a module composing a cell outside it is a configuration bug — rejected outright, not degraded to a `_runtime` sentinel. Mirrors K8s runtime.Scheme: registration-time enumeration + hard rejection of out-of-set identities.

func (*Builder) Build

func (b *Builder) Build(
	ctx context.Context,
	shared *SharedDeps,
	runtimeOptsFn RuntimeOptionsFunc,
) (*App, error)

func (*Builder) Migrations

func (b *Builder) Migrations() []MigrationRegistration

Migrations returns the registered migration sets in registration order, for the ops / composition-root entry point to apply before Build. Returns a copy so callers cannot mutate the builder's backing slice.

func (*Builder) With

func (b *Builder) With(modules ...CellModule) *Builder

With appends modules to the builder. Calls are accumulating: successive With calls append, they do not replace. Analogous to fx.Provide chaining.

func (*Builder) WithMigrations

func (b *Builder) WithMigrations(ns migration.Namespace, fsys fs.FS) *Builder

WithMigrations registers an external Cell module's migration set under ns. Calls accumulate (successive calls append). The registration is validated at Builder.Build (namespace validity, NOT the reserved "platform" namespace, non-nil FS, no duplicate namespace); Build does not execute the migrations.

The reserved migration.PlatformNamespace MUST NOT be used here — it is seeded automatically by adapters/postgres.NewMigrationSetWithPlatform. Passing it is rejected fail-fast at Build (and again at adapters/postgres.MigrationSet.Add), so an external module cannot clobber the platform schema lineage.

type CellModule

type CellModule interface {
	// ID returns a stable identifier used in error messages and logs.
	ID() string

	// Provide resolves Cell-specific dependencies from the shared context and
	// returns a [ModuleResult] (the constructed Cell + non-resource opts +
	// the single-source ManagedResource list) or an error.
	//
	// Resource lifecycle is single-source: the module lists every external
	// connection it opened (PG pool, vault client, …) in ModuleResult.Resources,
	// and [Builder.Build] derives BOTH the happy-path bootstrap.WithManagedResource
	// registration AND the pre-Run rollback from that one slice. The module MUST
	// NOT call bootstrap.WithManagedResource itself.
	//
	// Cross-module value handoff via the former ModuleExports type has been
	// removed (Wave-1 #1423). Cell modules are now fully self-contained; cross-cell
	// communication happens via events (contract-based), not via in-process
	// Go handle passing at composition time.
	//
	// MODULE-PROVIDE-NO-VALUE-HANDOFF-01 (tools/archtest/module_provide_signature_frozen_test.go)
	// enforces that this signature has exactly two inputs (context.Context, *SharedDeps)
	// and two outputs (ModuleResult, error), and that ModuleResult has exactly the
	// fields {Cell, Opts, Resources} with no cross-module value-handoff channel.
	Provide(ctx context.Context, shared *SharedDeps) (ModuleResult, error)
}

CellModule is the contract by which a Cell declares itself to Builder.Build. Each Cell provides a single *_module.go file that implements this interface, self-managing all Cell-specific dependency wiring (KeyProvider, PoolResource, cellOpts, etc.).

Cross-cell communication

Cells communicate with each other exclusively via contracts: HTTP contracts (request/response) and event contracts (publish/subscribe). In-process Go handle passing between Cells at composition time is prohibited — the former ModuleExports type has been removed (Wave-1 #1423). If a Cell needs to react to another Cell's state change, it subscribes to an event contract; it does not receive a direct reference to the other Cell's internal store or service.

See MODULE-PROVIDE-NO-VALUE-HANDOFF-01 archtest for the machine enforcement.

ref: uber-go/fx fx.Module(name, opts...) — each module is self-contained and registers its own providers. ref: Go proverbs "accept interfaces, return structs" — when there is only one concrete implementation, the interface is introduced at the point where a second impl appears; CellModule has multiple impls (one per Cell) so the interface is appropriate here.

type MigrationRegistration

type MigrationRegistration struct {
	// Namespace identifies the migration lineage; its tracking table is
	// schema_migrations_<namespace> (derived adapter-side). The reserved
	// "platform" namespace is the platform's own migrations and is seeded by the
	// ops entry point (adapters/postgres.NewMigrationSetWithPlatform), not here.
	Namespace migration.Namespace
	// FS is the embed.FS (or any fs.FS) of goose-native NNN_desc.sql files for
	// this namespace, with its own independent 001..N sequence.
	FS fs.FS
}

MigrationRegistration is a layer-neutral (namespace, fs.FS) migration source registered via Builder.WithMigrations. composition does NOT execute migrations — runtime/ must not depend on adapters/, and migrations run out-of-band BEFORE Build brings up cells (cells need their schema first). The registrations are surfaced via Builder.Migrations for an ops / composition-root entry point (which may import adapters/postgres) to drain into an adapters/postgres.MigrationSet and apply against a real database.

ref: docs/guides/cell-external-repo-quickstart.md "Migrations" — the execution-bridge loop external Cell modules write.

type ModuleResult

type ModuleResult struct {
	// Cell is the constructed Cell. Must be non-nil on success.
	Cell cell.Cell

	// Opts are non-resource bootstrap options the Cell needs (e.g.
	// bootstrap.WithRelay). Resources MUST NOT be registered here via
	// bootstrap.WithManagedResource — use the Resources field; the Builder
	// derives the WithManagedResource registration from it.
	Opts []bootstrap.Option

	// Resources are the ManagedResources opened during Provide. The Builder
	// derives both steady-state registration and pre-Run rollback from this one
	// slice (single source). Entries MUST be non-nil.
	Resources []kernellifecycle.ManagedResource
}

ModuleResult is the single-source result of CellModule.Provide.

It carries the constructed Cell plus the two kinds of bootstrap contribution a module can make:

  • Opts: non-resource bootstrap options (e.g. bootstrap.WithRelay). These flow into the assembly verbatim.
  • Resources: the ManagedResources the module opened during Provide (PG pool, vault client, rate-limiter cleanup goroutine, …). This is the SINGLE source for resource lifecycle: Builder.Build derives BOTH the steady-state registration (one bootstrap.WithManagedResource(r) per resource, so bootstrap.Run owns its Probes()/Worker()/LIFO-Close on the happy path) AND the pre-Run rollback stack (Close(ctx) in reverse order if a later module fails before bootstrap.Run starts). Because both are derived from the one slice, they can never diverge.

Modules MUST NOT call bootstrap.WithManagedResource themselves — that is forbidden inside cellmodules/ by WITHMANAGEDRESOURCE-CELLMODULE-FUNNEL-01. List the resource in Resources and the Builder funnels it. Before #1420 a module had to write the same resource into both an opt and a provisional return; forgetting either half leaked the resource or dropped its /readyz probe. The single Resources field closes that double-write.

No cross-module value-handoff field is permitted (no Exports): the former ModuleExports channel was removed in Wave-1 #1423 and MUST stay removed. The field set is frozen by MODULE-PROVIDE-NO-VALUE-HANDOFF-01.

type RuntimeOptionsFunc

type RuntimeOptionsFunc func(cells []cell.Cell) ([]bootstrap.Option, error)

RuntimeOptionsFunc lets the composition root supply the fully-assembled runtime bootstrap options (assembly, three listeners + auth, consumer base, health/metrics). It receives the constructed cells so the caller can build the assembly from them.

Listener and auth construction MUST live in the caller (cmd/ or examples/), never inside runtime/composition — this package is forbidden by AUTH-PLAN-04 from constructing auth plans (auth.NewAuthJWT / auth.NewAuthServiceToken …).

Typical implementation: build a bootstrap.Assembly from cells, call auth.NewAuthJWTFromAssembly(asm) to obtain the JWT auth plan, then return bootstrap.WithAssembly(asm), bootstrap.WithListener(...) and related options.

Returns (nil, nil) if there are no runtime-specific options to add.

See examples/corebundlestarter/run.go for a runnable RuntimeOptionsFunc.

type SharedDeps

type SharedDeps struct {

	// Clock is the single root clock instance threaded through every adapter,
	// service, and middleware. Tests inject clockmock.FakeClock; production
	// wires clock.Real() exactly once at the entry point.
	Clock clock.Clock

	// Topology is the resolved adapter-mode / storage-backend combination. It
	// must be obtained from bootstrap.NewTopology / TopologyFromEnv (it is a
	// sealed type); a zero Topology fails validation.
	Topology bootstrap.Topology

	// JWTIssuer signs access tokens for the accesscore cell.
	// Source: runtime/auth.NewJWTIssuer (via authconfig.NewJWTIssuerFromRegistry).
	JWTIssuer *auth.JWTIssuer

	// JWTVerifier validates inbound access tokens.
	// Source: runtime/auth.NewJWTVerifier (via authconfig.NewJWTVerifierFromRegistry).
	JWTVerifier *auth.JWTVerifier

	// MetricsProvider is the kernel-neutral metrics backend. Production
	// callers assign a *promadapter.MetricProvider (which satisfies
	// kernelmetrics.Provider); tests may use kernelmetrics.NopProvider{}.
	//
	// The field type is intentionally the interface — this package must not
	// import adapters/prometheus or github.com/prometheus/client_golang.
	MetricsProvider kernelmetrics.Provider

	// EventBus is the in-process event bus for publish and subscribe.
	EventBus *eventbus.InMemoryEventBus

	// ConfigEventCollector records config consumer process and settlement
	// metrics. It is genuinely cross-cell — consumed by accesscore, configcore,
	// AND the corebundle config-event consumer middleware — so it stays on the
	// shared bag (registered once against MetricsProvider, injected into all
	// consumers). It is NOT a configcore-specific field.
	ConfigEventCollector obmetrics.ConfigEventCollector

	// ConsumerClaimer coordinates outbox consumer idempotency.
	ConsumerClaimer idempotency.Claimer

	// PG is the assembly's single postgres capability provider. Nil in
	// non-postgres modes; cell modules take their in-memory path.
	PG capability.PGProvider

	// Redis is the assembly's shared redis capability provider. Nil in modes
	// without redis.
	Redis capability.RedisProvider

	// InternalHMACRing is the HMAC key ring for /internal/v1/* service-token
	// signing and verification. Promoted from cmd/corebundle's private
	// internalGuard struct so cell modules can sign outbound requests without
	// coupling to the cmd-private type.
	InternalHMACRing *auth.HMACKeyRing

	// NonceStore is the replay-defense store backing the /internal/v1/*
	// service-token guard. Promoted from cmd/corebundle's private internalGuard
	// struct (alongside InternalHMACRing) so that production control-plane
	// validation can introspect Kind() at the composition boundary: in adapter
	// mode "real" a NoopNonceStore is rejected, and an in-memory store requires
	// the single-pod acknowledgement. Nil is permitted in dev/test adapter
	// modes (the Kind checks are real-mode-only); see validateProductionControlPlane.
	//
	// Note: although validate() only checks NonceStore in real adapter mode, any
	// consumer that wires the internal-listener auth chain via
	// auth.NewAuthServiceToken must supply a non-nil NonceStore — that constructor
	// fail-fasts on nil regardless of adapter mode. In dev/test supply
	// auth.NewInMemoryNonceStore(...).
	NonceStore kauth.NonceStore

	// PrimaryHTTPAddr is the bind address for the public HTTP listener.
	PrimaryHTTPAddr string

	// InternalHTTPAddr is the bind address for the internal HTTP listener.
	InternalHTTPAddr string

	// HealthHTTPAddr is the bind address for the health+metrics listener.
	HealthHTTPAddr string

	// MetricsToken guards /metrics (X-Metrics-Token header).
	MetricsToken string

	// VerboseToken guards /readyz?verbose (X-Readyz-Token header).
	VerboseToken string

	// VerboseDisabled declares that /readyz?verbose must not be served.
	VerboseDisabled bool

	// HealthLocalOnly explicitly waives the loopback-only HealthHTTPAddr guard.
	// Must be false in production; set true only in tests or single-node
	// deployments where the health listener is deliberately bound to a
	// non-loopback address.
	HealthLocalOnly bool

	// ProjectRoot is the directory used by the devtools catalog endpoint.
	// Empty means devtools catalog is disabled; external callers that do not
	// need devtools functionality should leave this field unset.
	ProjectRoot string

	// ConfigKeyProvider is the configcore value-encryption key provider.
	//
	// This is the LAST configcore-specific field on the shared bag. It remains
	// here (rather than being self-built inside cellmodules/configcore like the
	// stale-cipher and eventbus-cache collectors) because the vault-transit
	// provider needs adapters/vault.TransitMetrics, which is built from a raw
	// github.com/prometheus/client_golang registry — and the
	// adapterPromCallerAllowlist governance posture (archtest
	// observability_metrics_test.go, #1085 Batch 4 Part A) keeps raw-prometheus
	// construction in cmd/ so cellmodules/configcore never imports client_golang.
	// configcore self-building the key provider is gated on #885 (migrating vault
	// TransitMetrics to the kernel MetricsProvider); once #885 lands this field
	// moves into configcore too and SharedDeps becomes fully cell-agnostic.
	// Refs #1413 / #885.
	//
	// Built in cmd/corebundle, passed to cellmodules/configcore.Module. Nil means
	// no key provider; in real adapter mode configcore rejects nil (NoopTransformer
	// is dev-only).
	ConfigKeyProvider kcrypto.KeyProvider
	// contains filtered or unexported fields
}

SharedDeps holds the cross-cutting dependencies consumed by every CellModule. It is the public, interface-only equivalent of cmd/corebundle's internal SharedDeps: all adapter-concrete types have been replaced with their kernel/runtime interface counterparts so that this package never imports adapters/ or prometheus/client_golang.

Sealed construction: SharedDeps carries an unexported validity marker (valid) that only NewSharedDeps stamps after running validate(). Builder.Build refuses any instance whose marker is unset, so a package-external struct literal — which can populate the exported fields but can never set the unexported marker — cannot be fed to Build. The single construction surface is NewSharedDeps, so an unvalidated dep set is unconstructable for the consumer. Bare literals remain legal for reads (e.g. unit tests that exercise a helper reading one field without ever calling Build); only Build gates on the marker.

Fields are flat (no concern-grouped sub-structs): SharedDeps is a composition-root bag whose fields cross consumer boundaries. Forcing a sub-struct layout would make cross-cutting consumptions look like boundary violations when in fact they are the natural shape of a composition root.

Fields are genuinely cross-cutting (consumed by multiple cells or the runtime itself), with one transitional exception — ConfigKeyProvider, the last configcore-specific field, whose removal is gated on #885 (see its godoc). The former configcore-specific metric fields (EventbusCacheCollector, ConfigStaleCipherInc) were removed in #1413: configcore now self-builds those collectors from MetricsProvider via runtime/observability/metrics, since they route through the kernel Provider (not raw prometheus) and so do not trip the "no client_golang in cellmodules" posture.

ref: uber-go/fx fx.Supply — shared values provided once to all modules. ref: kubernetes/kubernetes cmd/kube-apiserver/app/options/validation.go — all required fields validated in one place before startup.

func NewSharedDeps

func NewSharedDeps(d SharedDeps) (*SharedDeps, error)

NewSharedDeps validates a populated SharedDeps and returns a sealed copy. The returned *SharedDeps carries the unexported validity marker that Builder.Build requires; a package-external SharedDeps literal cannot set it, so Build rejects any instance not produced here. This is the single construction surface for a Build-able SharedDeps.

ref: kubernetes/kubernetes cmd/kube-apiserver/app/options/validation.go — validates all fields before any component is constructed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL