Documentation
¶
Overview ¶
Package api is the HTTP layer for Converge.
Every entity is a resource: a row with owner_id IS NULL is a root, and a row with owner_id NOT NULL is owned by another resource. The API surface is uniformly /api/<version>/resources/* — the version (apiPrefix, currently /api/v1) is in the path and echoed on every response via X-API-Version. A breaking change ships a /api/v2 surface alongside v1 rather than mutating v1 in place. The endpoint paths in the table below are shown without the version prefix for brevity.
Read/write split: the Server depends on two Repo interfaces (injected via Deps). Every pure GET handler reads from `s.readRepo` so UI traffic can be served by a Postgres read replica. Mutations (POST/PUT/DELETE, op invocations, audit-log emits) and any read-after-write that must observe the just-written row use `s.repo` (primary). When no replica is configured DepsFromPools aliases read = primary, so the split is a no-op.
File map (all in package api):
server.go Server struct, NewServer, Handler, OpenAPISpec,
registerRoutes (the API surface in one table),
humaConfig, spaHandler, bigBody.
ports.go Repo + role interfaces + compile-time conformance check.
schema_registry.go schemaRegistry: declared kind/reactor manifests +
JSON-Schema cache + kindKnown/servableSchema queries.
dto.go Every huma input/output struct (resourceInfo,
resourceFull, page inputs, topology types, …).
projection.go dbq.Resource → DTO mappers, label decoding,
owner-kind cache, summary aggregators.
middleware.go gzip, decompress, CORS.
schemas.go Per-kind JSON Schema cache, server-side spec
validation, /api/kinds* endpoints,
registerOpenAPIComponents (named refs in /docs).
handlers_objects.go CRUD + children for a resource by id.
handlers_pages.go Filtered + paginated roots and children pages.
handlers_summary.go Per-root and cross-cluster readiness/kind aggregates.
handlers_topology.go Group-by-label drill-down + label-key autocomplete.
handlers_subgraph.go N-hop dependency walk from a starting resource.
handlers_events.go Per-resource audit log.
handlers_graph.go Children + dependency edges of a single root.
handlers_ops.go Subresource verb invocations + listings.
handlers_clusterinfo.go Running-fleet registry (cluster view).
Endpoints exposed:
/api/version GET build version + API version(s) + Go runtime
/api/resources GET, POST paginated/filtered cross-cluster list; POST applies a spec (create-or-update by kind+name; 201 on create, 200 on update)
/api/resources/roots GET paginated/filtered roots (owner_id IS NULL)
/api/resources/summary GET cross-cluster readiness/kind aggregates
/api/cluster-members GET running-fleet registry (members, roles, shards, liveness)
/api/kinds GET every kind the providers declare (registered or still pending)
/api/kinds/{kind}/schema GET JSON Schema for a kind's spec/status
A resource is addressed by its PUBLIC (kind, name) — never the internal uuid
(k8s identity model; the uuid surfaces only as `uid` on the full detail read).
/api/resources/{kind}/{name} GET, DELETE full resource with spec; DELETE requests soft-delete
/api/resources/{kind}/{name}/reconcile POST re-pend a resource
/api/resources/{kind}/{name}/children GET paginated/filtered children of a resource
/api/resources/{kind}/{name}/dependencies GET upstream resources this one depends on
/api/resources/{kind}/{name}/graph GET children + dep edges (join by kind/name ref)
/api/resources/{kind}/{name}/summary GET readiness/kind aggregates under this owner
/api/resources/{kind}/{name}/changes GET children updated_at > since
/api/resources/{kind}/{name}/topology GET group-by-label hierarchy
/api/resources/{kind}/{name}/topology/leaves GET leaves at a topology path
/api/resources/{kind}/{name}/topology/keys GET label-key autocomplete
/api/resources/{kind}/{name}/subgraph GET N-hop dep walk
/api/resources/{kind}/{name}/events GET audit log per resource
/api/resources/{kind}/{name}/ops GET, POST list operations; POST invokes a verb (verb+input in body → 202 + created op row)
/api/resources/{kind}/{name}/ops/{op_uid} GET single operation row (opaque op handle)
/openapi.yaml, /openapi.json GET OpenAPI 3.1 document (huma-emitted)
/docs GET huma's built-in Stoplight Elements page
Index ¶
- Constants
- type ClusterReads
- type Deps
- type EventRepo
- type KindConfigStore
- type KindManifestStore
- type ListReads
- type OpsRepo
- type ProviderConfigStore
- type ReactorBindingStore
- type Repo
- type ResourceCommands
- type ResourceInfo
- type ResourceReads
- type Server
- func (s *Server) Close()
- func (s *Server) Handler() http.Handler
- func (s *Server) OpenAPISpec() *huma.OpenAPI
- func (s *Server) RebuildHandler()
- func (s *Server) SetBuildVersion(v string)
- func (s *Server) SetCORSAllowedOrigins(origins []string)
- func (r *Server) SetDeclaredSchemas(manifests []model.KindManifest)
- func (s *Server) SetOnManifestApply(fn func())
- func (s *Server) SetRateLimit(rps float64, burst int, trustProxy bool)
- func (r *Server) SetReactorSchemas(manifests []model.KindManifest)
- type TopologyReads
Constants ¶
const APIVersion = "v1"
APIVersion is the current API version. It is carried in the URL path (apiPrefix) and advertised on every response via the X-API-Version header, so clients can pin a version and detect mismatches. A breaking change bumps this to v2 and registers the new surface under /api/v2 alongside v1, rather than mutating v1 in place.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ClusterReads ¶
type ClusterReads interface {
ListClusterMembers(ctx context.Context) ([]store.ClusterMember, error)
}
ClusterReads is the running-fleet registry read (the cluster view).
type Deps ¶
type Deps struct {
Primary Repo // writes + read-after-write
Read Repo // UI GETs (replica or primary)
Commands ResourceCommands // write use-cases (create/patch/reconcile/delete)
Configs ProviderConfigStore // providerconfigs CRUD (runtime-editable config)
KindConfigs KindConfigStore // kind_config read (per-kind cap + resync)
Bindings ReactorBindingStore // reactor_bindings CRUD ("what reacts to what")
Manifests KindManifestStore // kind_manifest CRUD (the CRD)
ReadyProbe health.Pinger // /readyz DB ping (nil → readiness always OK)
}
Deps is the API server's injected dependency set: the data-access ROLE INTERFACES the handlers use (never a concrete *store.Store or *pgxpool.Pool) plus the readiness pinger for /readyz. The composition root (cmd/converge) constructs the concrete store(s) once and hands them in here as interfaces, so the API layer can be exercised against fakes with no live Postgres and the dependency graph is visible at the wiring site.
Write and read are split across two pools upstream: Primary carries writes + read-after-write, Read carries UI GETs (a replica when configured, else the same primary handle). ReadyProbe pings the PRIMARY (a pod that can't write can't do its job).
func DepsFromPools ¶
DepsFromPools builds a Deps from concrete pgx pools — the ONE place the API layer calls store.New. Every field is satisfied by *store.Store, but the caller (and the resulting Server) only ever sees the role interfaces. Pass read == nil (or read == primary) when no replica is configured; the read surface then routes to the primary. This is the production/integration convenience; unit tests can build Deps{} directly from fakes for any subset of the interfaces they exercise.
type EventRepo ¶
type EventRepo interface {
EmitEvent(ctx context.Context, e store.Event) error
ListEvents(ctx context.Context, resourceID uuid.UUID, cursorAt time.Time, cursorID int64, limit int) ([]store.EventRow, error)
}
EventRepo is the per-resource audit log family (emit on mutations + list).
type KindConfigStore ¶
type KindConfigStore interface {
GetKindConfig(ctx context.Context, kind model.Kind, kindVersion int) (store.KindConfig, bool, error)
ListKindConfig(ctx context.Context) ([]store.KindConfig, error)
UpsertKindConfig(ctx context.Context, k store.KindConfig) error
GetDefaultProviderConfig(ctx context.Context, kind model.Kind, kindVersion int) (store.DefaultProviderConfig, bool, error)
}
KindConfigStore is the read+write boundary for the kind_config table (the per-kind OPERATIONAL settings — concurrency cap + resync policy). The API joins the reads with the kind's default providerconfig so an operator sees both in one panel, without the two tables being fused in storage, and exposes an authoritative write so caps/resync are runtime-editable from the UI. *store.Store satisfies it.
type KindManifestStore ¶
type KindManifestStore interface {
UpsertKindManifest(ctx context.Context, m model.KindManifest) (int64, error)
GetKindManifest(ctx context.Context, kind model.Kind, kindVersion int) (model.KindManifest, bool, error)
ListKindManifests(ctx context.Context) ([]model.KindManifest, error)
// DeleteKindManifest removes one (kind, kindVersion) — refused (ErrKindVersionInUse)
// if a resource or exact-version-pinned binding still references it; cascades the
// version's provider configs. found=false = already gone (idempotent 404).
DeleteKindManifest(ctx context.Context, kind model.Kind, kindVersion int) (found bool, deletedConfigs int64, err error)
}
KindManifestStore is the read+write boundary for the kind_manifest table — the per-(kind, kindVersion) CRD definition of a kind (its JSON Schemas, declared reactions, finalizer + policy). An operator applies a manifest here (the two DB triggers derive kind_config + reactor_bindings); the reaction engine + KindManifestCache read it. *store.Store satisfies it.
type ListReads ¶
type ListReads interface {
GetChildrenByOwner(ctx context.Context, ownerID uuid.UUID, limit int) ([]store.GetChildrenByOwnerRow, error)
GetDepsByOwner(ctx context.Context, ownerID uuid.UUID, limit int) ([]store.GetDepsByOwnerRow, error)
GetResourcesChangedSince(ctx context.Context, arg store.GetResourcesChangedSinceParams) ([]store.GetResourcesChangedSinceRow, error)
ListResourcesPage(ctx context.Context, arg store.ListResourcesPageParams) ([]store.ListResourcesPageRow, error)
ListRootResourcesPage(ctx context.Context, arg store.ListRootResourcesPageParams) ([]store.ListRootResourcesPageRow, error)
CountResourcesFiltered(ctx context.Context, arg store.CountResourcesFilteredParams) (int64, error)
CountRootResourcesFiltered(ctx context.Context, arg store.CountRootResourcesFilteredParams) (int64, error)
CountChildrenByReadiness(ctx context.Context, ownerID uuid.UUID) ([]store.CountChildrenByReadinessRow, error)
CountResourcesByReadinessScoped(ctx context.Context, ownerIDs []uuid.UUID) ([]store.CountResourcesByReadinessScopedRow, error)
}
ListReads is the owner-scoped children, page/list, and count family — the paginated roots/children/summary handlers.
type OpsRepo ¶
type OpsRepo interface {
CreateOperationRow(ctx context.Context, resourceID uuid.UUID, verb string, input []byte, requestedBy string) (store.ResourceOperation, error)
GetOperation(ctx context.Context, id uuid.UUID) (store.ResourceOperation, error)
ListOperationsByResource(ctx context.Context, resourceID uuid.UUID, states []string, limit int) ([]store.ResourceOperation, error)
SetOperationRunning(ctx context.Context, id uuid.UUID) error
}
OpsRepo is the subresource-verb operations family (the only READ role that also writes — an operation row is created + advanced as verbs run).
type ProviderConfigStore ¶
type ProviderConfigStore interface {
UpsertProviderConfig(ctx context.Context, name string, kind model.Kind, kindVersion int, isDefault bool, spec json.RawMessage, data []byte) (store.ProviderConfig, bool, error)
GetProviderConfig(ctx context.Context, name string) (store.ProviderConfig, bool, error)
ListProviderConfigs(ctx context.Context, f store.ProviderConfigFilter, limit, offset int) ([]store.ProviderConfig, error)
CountProviderConfigs(ctx context.Context, f store.ProviderConfigFilter) (int64, error)
DeleteProviderConfig(ctx context.Context, name string) (bool, error)
}
ProviderConfigStore is the read+write boundary for the providerconfigs table (the runtime-editable per-kind config store). *store.Store satisfies it.
type ReactorBindingStore ¶
type ReactorBindingStore interface {
UpsertReactorBinding(ctx context.Context, b store.ReactorBinding) error
ListReactorBindings(ctx context.Context) ([]store.ReactorBinding, error)
DeleteReactorBinding(ctx context.Context, name string) (bool, error)
}
ReactorBindingStore is the read+write boundary for reactor_bindings — the runtime-editable "what reacts to what" rule table that drives the lifecycle-reactor spine. A binding is NOT a resource: it parameterises the dispatcher, which reads it fresh on every claim (no cache), so an edit takes effect live with no restart. *store.Store satisfies it.
type Repo ¶
type Repo interface {
ResourceReads
ListReads
TopologyReads
OpsRepo
EventRepo
ClusterReads
}
Repo is the composed read facade the HTTP server holds — the union of the read roles above. The Server carries one bound to the primary pool (read-after-write) and one to the read replica (UI GETs). A handler that only needs one family may depend on that role interface instead of the whole Repo.
type ResourceCommands ¶
type ResourceCommands interface {
ApplySpec(ctx context.Context, kind model.Kind, name string, spec json.RawMessage, labels map[string]string) (store.ApplyResult, error)
// ApplySpecWithConfig is ApplySpec plus a per-resource CUSTOM config
// attachment (the providerconfig name in provider_config_ref). Empty name
// means "no attachment", identical to ApplySpec. (kindVersion = 1.)
ApplySpecWithConfig(ctx context.Context, kind model.Kind, name string, spec json.RawMessage, labels map[string]string, providerConfigName string) (store.ApplyResult, error)
// ApplySpecKindVersionWithConfig pins the web-API kindVersion on CREATE (the apply path
// uses it so a new resource records the version it was authored against).
ApplySpecKindVersionWithConfig(ctx context.Context, kind model.Kind, kindVersion int, name string, spec json.RawMessage, labels map[string]string, providerConfigName string) (store.ApplyResult, error)
// ResolveResourceIDKindVersionByKindName resolves (id, current kindVersion, found) so the
// apply path can detect a breaking kindVersion FLIP (applied kindVersion != pinned kindVersion).
ResolveResourceIDKindVersionByKindName(ctx context.Context, kind model.Kind, name string) (uuid.UUID, int, bool, error)
// FlipResourceKindVersion performs the uuid-stable flip: rewrite the spec to the
// new kindVersion's shape and bump the resource's kindVersion + generation in place.
FlipResourceKindVersion(ctx context.Context, id uuid.UUID, oldKindVersion, newKindVersion int, newSpec json.RawMessage, kind model.Kind) (store.FlipResult, error)
Reconcile(ctx context.Context, id uuid.UUID) error
RequestResourceDeletion(ctx context.Context, id uuid.UUID, finalizer, actor string) (bool, error)
// QuarantineResource sets a failed/stuck resource aside (freezes it from all
// schedulers + unblocks its root's rollup, without deleting it).
QuarantineResource(ctx context.Context, id uuid.UUID, actor string) (bool, error)
// UnquarantineResource clears a quarantine and re-arms the resource.
UnquarantineResource(ctx context.Context, id uuid.UUID, actor string) (bool, error)
// RollbackSpec checks out a historic root revision (by authored
// generation) as the live spec, returning the resource's generation after
// checkout (0 if the revision wasn't found).
RollbackSpec(ctx context.Context, id uuid.UUID, generation int64, actor string) (int64, error)
}
ResourceCommands is the write/use-case boundary the HTTP server depends on for resource mutations (create / patch / re-pend / soft-delete). *store.Store satisfies it. It is scoped to just the data mutations the handlers need — "create a root", not a goroutine/process-lifecycle manager — so the API's write paths stay decoupled from the engine.
type ResourceInfo ¶
type ResourceInfo = resourceInfo
ResourceInfo is an EXPORTED alias of resourceInfo used only for embedding in rootPageItem below. Huma promotes the fields of an embedded struct into the parent's OpenAPI schema ONLY when the embedded field's name is exported; an embedded unexported `resourceInfo` would emit a schema with just resource_count while the JSON still carries the promoted kind/name/phase/… (a spec-vs-wire mismatch that breaks generated clients). Embedding the alias keeps the flat wire shape AND makes Huma reflect the full field set.
type ResourceReads ¶
type ResourceReads interface {
// Public-identity resolution: (kind, name) → internal id. Every name-keyed
// handler calls this once at its top, then delegates to the id-based reads.
ResolveResourceIDByKindName(ctx context.Context, kind model.Kind, name string) (uuid.UUID, error)
ResolveResourceIDsByKindName(ctx context.Context, refs []store.ObjectRef) ([]store.ResolvedRef, error)
// Per-resource reads. The single-resource reads are addressed by the PUBLIC
// (kind, name) and fold the id-resolve into the query itself (one indexed
// seek, no separate probe); GetResource returns r.id for the detail
// handler's own follow-ups (conditions, work_queue). GetResourceInfo stays
// id-based for the internal paths that already hold an id (apply read-back,
// owner-cache batch); GetResourceInfoByName is its (kind,name) counterpart
// used for the owner envelope (returns r.id, reused by the scoped query).
GetResource(ctx context.Context, kind model.Kind, name string) (store.GetResourceRow, error)
GetResourceInfo(ctx context.Context, id uuid.UUID) (store.GetResourceInfoRow, error)
GetResourceInfoByName(ctx context.Context, kind model.Kind, name string) (store.GetResourceInfoByNameRow, error)
GetResourcesInfo(ctx context.Context, ids []uuid.UUID) ([]store.GetResourcesInfoRow, error)
GetResourceManifest(ctx context.Context, kind model.Kind, name string) (store.GetResourceManifestRow, error)
GetResourceStatus(ctx context.Context, kind model.Kind, name string) (store.GetResourceStatusRow, error)
// Spec revision history (roots): list revisions (bodies elided) + fetch
// one historic body (by authored generation) for raw download / prefill.
// Both addressed by (kind, name).
ListSpecHistory(ctx context.Context, kind model.Kind, name string, limit int) ([]store.SpecHistoryRow, error)
GetSpecRevision(ctx context.Context, kind model.Kind, name string, generation int64) (json.RawMessage, error)
GetResourceDependencies(ctx context.Context, kind model.Kind, name string) ([]store.GetResourceDependenciesRow, error)
ListResourceConditions(ctx context.Context, resourceID uuid.UUID) ([]store.ListResourceConditionsRow, error)
GetWorkQueueByResource(ctx context.Context, resourceID uuid.UUID) ([]store.GetWorkQueueByResourceRow, error)
}
ResourceReads is the single-resource read family: identity resolution + the per-resource detail queries the object/history handlers call.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server depends on two Repo interfaces (injected via Deps):
- repo: primary (read-write). Every mutation handler (POST/PUT/DELETE, op invocations, audit-log emits) goes here, plus any read-after-write that must observe the just-written row.
- readRepo: read replica when one is configured. Every pure-GET handler reads from here so UI traffic doesn't compete with engine writes for primary connections.
When no replica is configured, DepsFromPools aliases read = primary so the split is a no-op at runtime.
func NewServer ¶
NewServer wires the API from an injected Deps bundle of role interfaces (see Deps). The per-kind schema surface (validation, finalizer, and verb lookups) is supplied separately via SetDeclaredSchemas — the API depends only on declared schemas, never the live provider registry.
func (*Server) Close ¶
func (s *Server) Close()
Close releases the server's background resources (currently the rate limiter's janitor goroutine). Safe to call on a server that never enabled a limiter, and safe to call more than once. Wire it to the process shutdown path.
func (*Server) Handler ¶
Handler returns the long-lived HTTP handler to mount on the listener. It is a thin, stable delegator: it reads the CURRENT inner handler from s.live on each request and serves it. The inner handler is built by buildHandler and can be atomically replaced by RebuildHandler (e.g. after a manifest apply) so the OpenAPI surface reflects a runtime-applied CRD — without re-binding the listener. Call once; subsequent calls return a delegator over the same holder.
func (*Server) OpenAPISpec ¶
OpenAPISpec returns the assembled spec. Used by the golden test in openapi_test.go; not part of the runtime serving path.
func (*Server) RebuildHandler ¶
func (s *Server) RebuildHandler()
RebuildHandler rebuilds the inner handler from the server's CURRENT schema surface and atomically swaps it into the live holder, so the next /docs and /openapi request serializes a fresh OpenAPI document that includes a runtime-applied kind's schemas. huma caches the serialized spec for an API instance's life, so a new instance (a new mux + huma.New) is the only way to surface a post-boot CRD in /docs — an in-place component-map mutation would be masked by that cache. Safe to call concurrently with serving (in-flight requests keep the handler they read at dispatch). No-op before Handler().
func (*Server) SetBuildVersion ¶
SetBuildVersion sets the binary version string the /version endpoint reports. Call before Handler().
func (*Server) SetCORSAllowedOrigins ¶
SetCORSAllowedOrigins configures the cross-origin allowlist (see the field doc). Call before Handler(). Empty = no CORS headers (same-origin only).
func (*Server) SetDeclaredSchemas ¶
func (r *Server) SetDeclaredSchemas(manifests []model.KindManifest)
SetDeclaredSchemas records the WORK kinds this deployment declares (their applied manifests, incl. still-pending ones) and (re)builds the JSON-Schema cache, publishing a fresh snapshot atomically. Reactor state (if any) is carried over from the current snapshot, and the union cache is rebuilt to include it.
func (*Server) SetOnManifestApply ¶
func (s *Server) SetOnManifestApply(fn func())
SetOnManifestApply registers a callback invoked after a successful /api/kinds/{kind}/manifest PUT, so the deployment can rebuild its schema validator from the new manifest set. nil-safe; tests may skip it.
func (*Server) SetRateLimit ¶
SetRateLimit enables per-client-IP API rate limiting: rps tokens/sec with a burst bucket. rps <= 0 (the default) leaves limiting OFF — no limiter is built and buildHandler registers no middleware, so there is zero cost. burst <= 0 defaults to rps (at least 1). trustProxy makes the client key come from X-Forwarded-For / X-Real-IP (set it only behind a proxy that overwrites those, else a client can forge them). Call before Handler(); Close stops the limiter's idle-eviction janitor.
func (*Server) SetReactorSchemas ¶
func (r *Server) SetReactorSchemas(manifests []model.KindManifest)
SetReactorSchemas records the REACTOR kinds this deployment declares and folds their shapes into the JSON-Schema cache so /api/kinds/{kind}/schema can serve a reactor's CONFIG schema. Reactors stay OUT of `declared` (the creatable-resource gate) because a reactor is bound to other kinds' transitions, not created directly. Carries the current work state forward and publishes a fresh snapshot atomically. Optional — a deployment with no reactors need not call it.
type TopologyReads ¶
type TopologyReads interface {
AggregateTopology(ctx context.Context, ownerID uuid.UUID, path []store.TopologyPathSegment, nextKey, cursor string, limit int) ([]store.TopologyBucket, error)
CountTopologyBuckets(ctx context.Context, ownerID uuid.UUID, path []store.TopologyPathSegment, nextKey string) (int, error)
TopologyLeaves(ctx context.Context, ownerID uuid.UUID, path []store.TopologyPathSegment, cursorName string, cursorID uuid.UUID, limit int) ([]store.GetChildrenByOwnerRow, error)
TopologyLabelKeys(ctx context.Context, ownerID uuid.UUID, limit int) ([]string, error)
SubgraphTransitive(ctx context.Context, kind model.Kind, name string, ancestorDepth, descendantDepth, limit int) ([]store.SubgraphTransitiveRow, error)
SubgraphDeps(ctx context.Context, ids []uuid.UUID) ([]store.SubgraphDep, error)
}
TopologyReads is the group-by-label drill-down + subgraph family.
Source Files
¶
- cursor.go
- dto_cluster.go
- dto_ops.go
- dto_page.go
- dto_resource.go
- dto_topology.go
- errors.go
- handlers_clusterinfo.go
- handlers_events.go
- handlers_graph.go
- handlers_history.go
- handlers_kind_manifest.go
- handlers_objects.go
- handlers_ops.go
- handlers_pages.go
- handlers_providerconfigs.go
- handlers_raw.go
- handlers_reactor_bindings.go
- handlers_schema.go
- handlers_subgraph.go
- handlers_summary.go
- handlers_topology.go
- handlers_version.go
- listcap.go
- middleware.go
- phases.go
- ports.go
- projection.go
- projection_cluster.go
- projection_conditions.go
- projection_labels.go
- projection_owner.go
- projection_page.go
- projection_work.go
- ratelimit.go
- resolve.go
- schema_cache.go
- schema_openapi.go
- schema_registry.go
- server.go