Help us shape the future of Go! Take the 2026 Go Developer Survey

api

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

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

View Source
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

func DepsFromPools(primary, read *pgxpool.Pool) Deps

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

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

func NewServer(deps Deps) *Server

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

func (s *Server) Handler() http.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

func (s *Server) OpenAPISpec() *huma.OpenAPI

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

func (s *Server) SetBuildVersion(v string)

SetBuildVersion sets the binary version string the /version endpoint reports. Call before Handler().

func (*Server) SetCORSAllowedOrigins

func (s *Server) SetCORSAllowedOrigins(origins []string)

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

func (s *Server) SetRateLimit(rps float64, burst int, trustProxy bool)

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.

Jump to

Keyboard shortcuts

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