Documentation
¶
Overview ¶
Package resource provides a single generic HTTP handler wiring for every v1alpha1 kind. One call to Register() binds the per-kind endpoints, backed by a generic v1alpha1store.Store and a typed envelope T.
Route shape (flat; namespace is a query param, defaults to "default"; `?namespace=all` widens list scope to every namespace):
GET {basePrefix}/{pluralKind}?namespace={ns} list
GET {basePrefix}/{pluralKind}/{name}?namespace={ns} get latest
GET {basePrefix}/{pluralKind}/{name}/tags?namespace={ns} list tags of one (tagged content kinds only)
GET {basePrefix}/{pluralKind}/{name}/{tag}?namespace={ns} get exact tag (tagged content kinds only)
PUT {basePrefix}/{pluralKind}/{name}?namespace={ns} apply mutable object (Provider/Deployment/config)
DELETE {basePrefix}/{pluralKind}/{name}?namespace={ns} delete mutable object
DELETE {basePrefix}/{pluralKind}/{name}/{tag}?namespace={ns} delete exact tag (tagged content kinds only)
Direct PUT is registered only for mutable object stores. Content-registry artifact kinds (Agent, MCPServer, Model, Plugin, Skill, Prompt) use metadata.tag and are written through POST /v0/apply.
Index ¶
- func ApplyObject(ctx context.Context, cfg ApplyConfig, obj v1alpha1.Object, dryRun bool) arv0.ApplyResult
- func DeleteObject(ctx context.Context, cfg ApplyConfig, obj v1alpha1.Object, dryRun bool) arv0.ApplyResult
- func ProductionAdmission(ctx context.Context, in types.AdmissionInput) (types.AdmissionResult, error)
- func ProductionDeleteAdmission(ctx context.Context, in types.DeleteAdmissionInput) (types.DeleteAdmissionResult, error)
- func Register[T v1alpha1.Object](api huma.API, cfg Config, newObj func() T)
- func RegisterApply(api huma.API, cfg ApplyConfig)
- type ApplyConfig
- type AuthorizeInput
- type Config
- type ListInput
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ApplyObject ¶
func ApplyObject(ctx context.Context, cfg ApplyConfig, obj v1alpha1.Object, dryRun bool) arv0.ApplyResult
ApplyObject runs one already-decoded object through the same production apply path used by POST /v0/apply. Downstream routes can call this to replay a previously accepted object without duplicating validation, authz, persistence, or post-upsert behavior.
func DeleteObject ¶
func DeleteObject(ctx context.Context, cfg ApplyConfig, obj v1alpha1.Object, dryRun bool) arv0.ApplyResult
DeleteObject runs one already-decoded object through the same production delete path used by DELETE /v0/apply.
func ProductionAdmission ¶
func ProductionAdmission(ctx context.Context, in types.AdmissionInput) (types.AdmissionResult, error)
ProductionAdmission is the OSS admission implementation: dry-runs stop after validation, and real writes upsert the object into the production store and run the per-kind post-upsert hook.
func ProductionDeleteAdmission ¶
func ProductionDeleteAdmission(ctx context.Context, in types.DeleteAdmissionInput) (types.DeleteAdmissionResult, error)
ProductionDeleteAdmission is the OSS delete admission implementation. It removes the selected production row(s) and then runs the per-kind post-delete hook when present.
func Register ¶
Register wires the namespace-scoped + cross-namespace list endpoints for kind T. newObj must return a fresh, zero-valued T on each call (e.g. `func() *v1alpha1.Agent { return &v1alpha1.Agent{} }`).
func RegisterApply ¶
func RegisterApply(api huma.API, cfg ApplyConfig)
RegisterApply wires POST {BasePrefix}/apply and DELETE {BasePrefix}/apply.
POST: for each document, stamps TypeMeta, validates, resolves refs (when Resolver is set), runs registry + uniqueness checks, and Upserts via the kind-matched Store.
DELETE: for each document, calls Store.Delete on the named resource. Tagged artifacts use metadata.tag when supplied; omitted tag deletes all tags for that namespace/name. Mutable objects delete by namespace/name. Validation still runs so clients get the same error surface as apply.
Both endpoints always return 200 with a per-document Results slice; document-level failures are surfaced as Status="failed" entries and do not short-circuit the batch. Callers diff Results to decide whether to retry.
Types ¶
type ApplyConfig ¶
type ApplyConfig struct {
// BasePrefix is the HTTP route prefix shared with the generic resource
// handler (e.g. "/v0"). The apply endpoint mounts at
// "{BasePrefix}/apply".
BasePrefix string
// Stores maps Kind ("Agent", "MCPServer", etc.) to its Store.
Stores map[string]*v1alpha1store.Store
// Resolver is forwarded to each decoded object's ResolveRefs.
Resolver v1alpha1.ResolverFunc
// RegistryValidator is forwarded to each decoded object's
// ValidateRegistries. Nil skips external-registry validation.
RegistryValidator v1alpha1.RegistryValidatorFunc
// Scheme decodes the incoming YAML/JSON stream. Defaults to
// v1alpha1.Default when nil.
Scheme *v1alpha1.Scheme
// Authorizers, when non-empty, gates each decoded document on apply
// against the same per-kind hook the generic resource handler
// consults on direct mutable-object PUT. Without this,
// /v0/apply (the multi-doc batch endpoint arctl uses) bypasses the
// per-kind authz wired through crud.PerKindHooks. Missing keys
// authorize-allow (matches resource.Config.Authorize == nil).
//
// Each document gets its own AuthorizeInput (Verb="apply", Kind +
// Name + Tag + Namespace from the decoded metadata) so the
// caller can deny per-resource. Errors fail the document; the rest
// of the batch continues — same per-doc isolation the upsert path
// already has.
Authorizers map[string]func(ctx context.Context, in AuthorizeInput) error
// PostUpserts mirrors resource.Config.PostUpsert per kind for extension
// hooks. Built-in Deployment apply is controller-owned and does not use
// this synchronous surface.
PostUpserts map[string]func(ctx context.Context, obj v1alpha1.Object) error
// PostDeletes mirrors resource.Config.PostDelete per kind for extension
// hooks. Built-in Deployment teardown is controller-owned and does not use
// this synchronous surface.
PostDeletes map[string]func(ctx context.Context, obj v1alpha1.Object) error
// InitialFinalizers mirrors resource.Config.InitialFinalizers per kind.
InitialFinalizers map[string]func(obj v1alpha1.Object) []string
// Source labels the producer of objects entering this apply pipeline.
// Empty defaults to types.AdmissionSourceApply.
Source string
// Admission optionally owns the final apply write. Nil uses
// ProductionAdmission, which writes to the configured production Store.
Admission types.Admission
// DeleteAdmission optionally owns the final delete. Nil uses
// ProductionDeleteAdmission, which deletes from the configured production
// Store and runs the per-kind PostDelete hook.
DeleteAdmission types.DeleteAdmission
// Prepare optionally mutates an object after validation and before
// admission. Import uses this to merge scanner output while still
// persisting through the shared apply path.
Prepare func(ctx context.Context, obj v1alpha1.Object) error
}
ApplyConfig is the per-server configuration for the multi-doc apply endpoints. Stores maps a v1alpha1 Kind to the matching v1alpha1store.Store. Resolver optionally checks cross-kind ResourceRef existence; when nil ResolveRefs is skipped.
type AuthorizeInput ¶
type AuthorizeInput struct {
// Verb is "get" | "list" | "apply" | "delete".
Verb string
// Kind is the canonical Kind the handler is serving (e.g. "Role").
Kind string
// Namespace is the URL-scoped namespace; empty for the cross-namespace
// list endpoint.
Namespace string
// Name is empty for list verbs.
Name string
// Tag is populated for exact tagged content resource operations.
// Batch delete leaves Tag empty when deleting every tag for a name.
Tag string
// Object is non-nil only when Verb == "apply"; it carries the decoded
// request body post-validation-stamping (path identity already merged
// into metadata), so the hook can inspect labels / annotations / spec
// in authz decisions.
Object v1alpha1.Object
}
AuthorizeInput is the context passed to Config.Authorize on every handler invocation. Fields are populated per the verb being authorized (see Config.Authorize comment for the combinations). New fields may be added in future releases — callers should use named-field initialization and tolerate unknown verbs by defaulting to deny.
type Config ¶
type Config struct {
// Kind is the canonical Kind name (e.g. v1alpha1.KindAgent = "Agent").
Kind string
// PluralKind is the lowercase plural used in route paths (e.g. "agents",
// "mcpservers"). If empty, defaults to strings.ToLower(Kind) + "s".
PluralKind string
// BasePrefix is the HTTP route prefix shared across kinds (e.g. "/v0").
// Routes extend it with `/{plural}/{name}` and, for tagged artifacts,
// `/{plural}/{name}/{tag}`; namespace is
// carried as a query param (`?namespace={ns}`, default "default").
BasePrefix string
// Store is the v1alpha1store.Store bound to this kind's table. Callers
// construct one Store per kind; this package does not create them.
Store *v1alpha1store.Store
// Resolver is optional; when set, the apply handler calls
// obj.ResolveRefs with it so dangling references surface as 400
// errors. Leave nil to skip ref resolution (e.g. for kinds with no
// ResourceRef fields).
Resolver v1alpha1.ResolverFunc
// RegistryValidator is optional; when set, the apply handler
// calls obj.ValidateRegistries with it so external-registry
// failures (package missing, OCI label mismatch, etc.) surface
// as 400 errors. Leave nil to skip registry validation (tests,
// offline imports, air-gapped servers).
RegistryValidator v1alpha1.RegistryValidatorFunc
// PostUpsert is optional; when set, the apply handler invokes it
// after a successful Upsert + read-back so the kind can drive
// post-persist reconciliation. Built-in Deployment adapter side effects
// are not wired through this hook; they are owned by the Deployment
// controller's asynchronous reconcile loop.
//
// Hook errors surface as 500 — the row is already persisted, so a
// failure here indicates degraded state the caller should retry.
//
// Known limitation: Store.Upsert commits its own transaction before the
// hook fires, so a hook failure leaves the row persisted with stale Status
// (whatever the previous reconcile wrote). The caller sees a 500, but a
// follow-up GetLatest still returns the row.
//
// The hook re-fires on every PUT — including identical-spec
// re-applies that are a no-op at the Store layer — because the
// handler unconditionally invokes PostUpsert after Upsert
// returns, without consulting the upsert change-status. This is
// the operator-friendly retry path: a transient runtime-adapter
// failure clears as soon as the operator re-applies (or a periodic
// CI re-apply succeeds), without forcing a spec bump.
//
// The generic hook failure contract is pinned by
// TestResourceRegister_PostUpsertFailureLeavesPersistedRow.
PostUpsert func(ctx context.Context, obj v1alpha1.Object) error
// PostDelete is optional; when set, the delete handler invokes it
// after Store.Delete (which sets DeletionTimestamp). The row still
// exists at this point — the soft-delete + GC pass owns hard
// removal. Built-in Deployment teardown is controller-owned and does not
// use this hook.
PostDelete func(ctx context.Context, obj v1alpha1.Object) error
// Prepare is optional; when set, the apply handler invokes it after
// validation (refs/registries) and before admission/Store.Upsert, so
// the kind can mutate the decoded object before it is persisted (e.g.
// strip sensitive spec fields). Runs on both the dedicated PUT
// route and the batch /v0/apply path. Hook errors short-circuit the
// write and surface to the caller.
Prepare func(ctx context.Context, obj v1alpha1.Object) error
// DeleteAdmission optionally owns the final delete after authz. Nil uses
// ProductionDeleteAdmission, which deletes from the configured Store and
// runs PostDelete.
DeleteAdmission types.DeleteAdmission
// InitialFinalizers, when non-nil, seeds finalizers atomically on create.
// Updates preserve existing finalizers.
InitialFinalizers func(obj v1alpha1.Object) []string
// Authorize is optional; when set, every read and write handler
// (get / list / apply / delete) invokes it as an access gate before
// touching the store. Return nil to allow; return a huma error
// (Error401Unauthorized / Error403Forbidden / etc.) to reject — the
// value propagates back to the client as-is so the hook controls the
// status code. Wrap a non-huma error in huma.Error500InternalServerError
// if you want the server to 500.
//
// nil hook matches the OSS default: public reads and writes, with
// authorization deferred to router-level middleware or the underlying
// auth.AuthzProvider. Downstream builds that need per-kind gates
// (e.g. "only registry admins can mutate Role") wire this callback.
//
// The hook is called after path parsing and — for apply — after the
// body decodes, but before any validation or store I/O. For list +
// cross-namespace list, Name and Tag are empty; for get-latest,
// Tag is empty; Object is non-nil only for apply.
Authorize func(ctx context.Context, in AuthorizeInput) error
// ListFilter is optional; when set, list handlers consult it before
// querying the store and inject the returned predicate into
// ListOpts.ExtraWhere / ExtraArgs. This is the per-row authz seam —
// downstream integrations wire it to a per-user RBAC predicate so a
// reader without grant for a given resource never sees the row in
// the list response, but reads at the row endpoint still 403 via
// Authorize.
//
// Returning a nil error + empty fragment means "no extra filter,
// behave like the public default". A non-nil error short-circuits
// the list and propagates to the caller (use a huma error to set
// the response code; non-huma errors bubble as 500).
//
// Mirrors the contract on v1alpha1store.ListOpts.ExtraWhere — read
// the placeholder + parameterization rules there before wiring a
// new caller.
ListFilter func(ctx context.Context, in AuthorizeInput) (extraWhere string, extraArgs []any, err error)
// EnableOriginFilter exposes ?origin=managed|discovered on list routes
// for kinds that distinguish registry-managed rows from provider-discovered
// rows materialized into the same Store. Leave false for regular resource
// lists.
EnableOriginFilter bool
// IncludeTerminatingByDefault, when true, makes the list handler
// surface rows with deletion_timestamp set even if the caller
// hasn't passed ?includeTerminating=true. Used by kinds whose
// teardown is operator-observable so `arctl get` can
// show resources while finalizers are still draining.
//
// The ?includeTerminating query value is OR-ed with this flag, so
// the caller can still force inclusion but never exclusion when
// the kind has opted in.
IncludeTerminatingByDefault bool
}
Config is the per-kind configuration for Register. Kind / BasePrefix / Store are required; Resolver is optional (enables cross-kind ref existence checks on apply).
type ListInput ¶
type ListInput struct {
// Namespace scopes the list. Empty / missing → "default";
// literal "all" → cross-namespace.
Namespace string `query:"namespace" doc:"Namespace (defaults to 'default'; 'all' lists across all namespaces)."`
Limit int `query:"limit" doc:"Max items to return (default 50)." default:"50"`
Cursor string `query:"cursor" doc:"Opaque pagination cursor."`
Labels string `query:"labels" doc:"Label selector: key=value,key2=value2."`
Tag string `query:"tag" doc:"Restrict the result set to one tag value (tagged artifact kinds only)."`
LatestOnly bool `query:"latestOnly" doc:"Only return the literal latest tag per (namespace, name). Equivalent to tag=latest for tagged kinds."`
// IncludeTerminating surfaces soft-deleted rows (deletionTimestamp != nil)
// which are hidden by default.
IncludeTerminating bool `query:"includeTerminating" doc:"Include rows with a deletionTimestamp."`
}
ListInput defines the common list query parameters used by Huma route inputs. It is exported so Huma can reflect it when embedded by route-specific inputs.