v1alpha1store

package
v0.4.0 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const ControlPlaneNotifyChannel = "v1alpha1_control_plane_changed"

ControlPlaneNotifyChannel is the single coarse wakeup channel for controllers. The payload is only a hint; controllers must replay control_plane_events and re-read canonical source rows.

View Source
const DefaultTagValue = "latest"
View Source
const MigrationsDir = "migrations"

MigrationsDir is the directory inside MigrationFiles holding NNN_name.up.sql / NNN_name.down.sql pairs. Exported so the CLI and the orchestrator can pass it alongside MigrationFiles.

Variables

View Source
var ErrInvalidCursor = errors.New("v1alpha1 store: invalid cursor")

ErrInvalidCursor reports that a list pagination cursor could not be parsed.

View Source
var ErrInvalidExtraWhere = errors.New("v1alpha1 store: ExtraWhere / ExtraArgs placeholder mismatch")

ErrInvalidExtraWhere reports that ListOpts.ExtraWhere references more placeholders than ExtraArgs has bind values (or vice versa), which would either be a silent misuse or a runtime pgx error.

View Source
var ErrTerminating = errors.New("v1alpha1 store: object is terminating")

ErrTerminating reports that an Upsert targeted a row whose deletion_timestamp is set — the row is mid-teardown and cannot be mutated until its finalizers drain and the GC pass hard-deletes it. Matches Kubernetes semantics: `kubectl apply` against a terminating object returns 409 AlreadyExists ("object is being deleted; delete and recreate").

View Source
var MigrationFiles fs.FS = v1alpha1MigrationFiles

MigrationFiles is the embedded FS containing every OSS migration. Exported so callers (the CLI, the orchestrator, downstream tooling) can compute pending-migration counts and pass the embed to `golang-migrate`'s iofs source without piercing migrate.Migrate's internals.

Functions

func ContentHash

func ContentHash(meta *v1alpha1.ObjectMeta, spec json.RawMessage) (string, error)

ContentHash returns the canonical digest used for same-tag replacement detection. It deliberately includes only user-authored declarative state: spec plus labels/annotations.

func DefaultTag

func DefaultTag() string

DefaultTag returns the tag assigned when metadata.tag is omitted.

func NewOSSMigrator

func NewOSSMigrator(ctx context.Context, dsn string) (*migrate.Migrate, error)

NewOSSMigrator constructs a `*migrate.Migrate` against `database.OSSSchema` for the OSS migration set. The caller owns `mg.Close()`. ctx is accepted for API symmetry with the surrounding startup path.

func NewStores

func NewStores(pool *pgxpool.Pool, schemas *pkgdb.SchemaRegistry, opts ...StoreOption) map[string]*Store

NewStores builds one *Store per OSS built-in v1alpha1 Kind, bound to its canonical table. The returned map is keyed by Kind name (e.g. "Agent", "MCPServer") and is the single input the router/apply layers take. They never look up tables by string literal themselves.

Kinds whose descriptors use KindStorageMutableObject are bound through NewMutableObjectStore. Every other built-in kind uses NewStore (tagged-artifact behavior). Extension kinds are intentionally not built here; the composition root wires them from V1Alpha1StoreTables after this function returns.

The variadic opts are applied to every Store produced. Downstream callers pass WithAuditor(...) here to plumb a single audit sink across all kinds in one call.

func SpecHash

func SpecHash(raw json.RawMessage) string

SpecHash returns a deterministic SHA-256 hex digest of a JSON spec. Field order and whitespace do not affect the result; only the content (keys + values) does. Empty/null specs hash to a stable sentinel.

Types

type ControlPlaneEvent

type ControlPlaneEvent struct {
	Revision    int64
	Key         ResourceKey
	UID         string
	Generation  int64
	Operation   string
	CommittedAt time.Time
}

ControlPlaneEvent records that a canonical v1alpha1 source row changed after a monotonic revision. It intentionally carries identity only, not object payload or derived desired state.

type ControlPlaneEventStore

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

ControlPlaneEventStore reads and prunes the durable invalidation cursor used by controllers.

func NewControlPlaneEventStore

func NewControlPlaneEventStore(pool *pgxpool.Pool, schema pkgdb.Schema) *ControlPlaneEventStore

NewControlPlaneEventStore constructs a control-plane event reader.

func (*ControlPlaneEventStore) CurrentRevision

func (s *ControlPlaneEventStore) CurrentRevision(ctx context.Context) (int64, error)

CurrentRevision returns the current high-water revision, or 0 when the event table is empty.

func (*ControlPlaneEventStore) ListAfter

func (s *ControlPlaneEventStore) ListAfter(ctx context.Context, afterRevision int64, limit int) ([]ControlPlaneEvent, error)

ListAfter returns events with revision > afterRevision, ordered by revision.

func (*ControlPlaneEventStore) OldestRevision

func (s *ControlPlaneEventStore) OldestRevision(ctx context.Context) (revision int64, ok bool, err error)

OldestRevision returns the oldest retained event revision. ok=false means the table is empty.

func (*ControlPlaneEventStore) PruneBefore

func (s *ControlPlaneEventStore) PruneBefore(ctx context.Context, before time.Time, keepAfterRevision int64, limit int) (int64, error)

PruneBefore deletes retained events in bounded batches. At least one of before or keepAfterRevision must be set. Controllers must use gap detection before relying on pruning in production.

type FindReferrersOpts

type FindReferrersOpts struct {
	// Namespace, when non-empty, restricts results to a single namespace.
	Namespace string
	// LatestOnly, when true, restricts to the literal "latest" tag per
	// (namespace, name), or the private latest row for mutable-object stores.
	LatestOnly bool
	// IncludeTerminating, when true, keeps rows whose deletion_timestamp
	// is set. Default (false) excludes them.
	IncludeTerminating bool
}

FindReferrersOpts controls the FindReferrers scan.

type ListOpts

type ListOpts struct {
	// Namespace narrows results to a specific namespace. Empty means "across
	// all namespaces".
	Namespace string
	// LabelSelector narrows results to rows whose labels JSONB contains
	// this subset (uses `@>` with a GIN index).
	LabelSelector map[string]string
	// Limit caps the number of rows returned. Zero means default (50).
	Limit int
	// Cursor is an opaque pagination token. Empty starts from the beginning.
	Cursor string
	// Tag restricts the result set to a single tag value on tagged-artifact
	// stores. Empty means "no tag filter" — every tag of every name is
	// returned. Ignored on mutable-object stores (they have no tag column).
	// Mutually exclusive with LatestOnly (validated at the caller level;
	// the store treats LatestOnly as the literal `Tag = "latest"` filter
	// when both are set, but new callers should pick one).
	Tag string
	// LatestOnly restricts to the literal "latest" tag per (namespace, name),
	// or the private latest row for mutable-object stores. Equivalent to
	// `Tag = "latest"` on tagged stores; kept as a separate field because
	// it also covers the mutable-object latest-row case (where there's no
	// user-facing tag column).
	LatestOnly bool
	// IncludeTerminating includes rows with deletion_timestamp set. Default
	// false — callers asking for "alive" rows shouldn't see terminating ones.
	IncludeTerminating bool
	// ExtraWhere appends a caller-supplied parameterized SQL predicate to
	// the WHERE clause. It's the RBAC / tenancy / extension-filter seam:
	// the generic Store stays kind-agnostic while a wrapper injects
	// authz-derived constraints like `namespace = ANY($1)`.
	//
	// Rules:
	//   - Placeholders are numbered from `$1` relative to ExtraArgs (so
	//     the fragment reads naturally on its own). The Store rebases them
	//     to continue after its own internal $N before executing.
	//   - The placeholder count in the fragment MUST equal len(ExtraArgs).
	//     List returns ErrInvalidExtraWhere when they disagree.
	//   - NEVER interpolate untrusted input into ExtraWhere with
	//     fmt.Sprintf/string concatenation — always use placeholders with
	//     ExtraArgs. Doing otherwise is a SQL injection; this is the
	//     authz surface.
	//   - The fragment is appended with a leading AND, so a single
	//     standalone predicate like "deleted_by IS NULL" is fine; don't
	//     prefix with "AND " yourself.
	ExtraWhere string
	// ExtraArgs are the bind parameters for ExtraWhere. Number of entries
	// MUST equal the distinct placeholder count in ExtraWhere.
	ExtraArgs []any
}

ListOpts controls paginated list queries.

type PatchOpts

type PatchOpts struct {
	Status      func(current json.RawMessage) (json.RawMessage, error)
	Annotations func(map[string]string) map[string]string
	Finalizers  func([]string) []string
}

PatchOpts bundles optional column mutations applied atomically by ApplyPatch. Nil mutators skip the corresponding column entirely; the row's other fields are never touched.

type ResourceKey

type ResourceKey struct {
	Kind      string
	Namespace string
	Name      string
	Tag       string
}

ResourceKey identifies a source row in the v1alpha1 control plane.

type Store

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

Store is the single generic persistence layer for every v1alpha1 kind. One Store instance is bound to one table; callers construct one per kind (v1alpha1.agents, v1alpha1.mcp_servers, etc.).

Store has two private behaviors, picked at construction time:

  • TaggedArtifactStore (the default; produced by NewStore). Storage key is (namespace, name, tag). Users may supply the tag declaratively; missing tags are filled with the literal "latest". Re-applying the same tag replaces the prior row atomically when the content changes. Used for agents, mcp_servers, skills, and prompts.

  • MutableObjectStore (produced by NewMutableObjectStore). Storage key is (namespace, name). Used for Runtime/Deployment and additional downstream mutable control-plane/config kinds.

PatchStatus is disjoint from Upsert: it touches only status and updated_at, never spec. Reconcilers use PatchStatus exclusively; apply handlers use Upsert exclusively.

Delete hard-deletes tagged-artifact rows and mutable rows without finalizers. Mutable rows with finalizers are marked terminating via deletion_timestamp; exact Get can still load them, while GetLatest/List hide them unless the caller explicitly includes terminating rows. PurgeFinalized removes terminating mutable rows after finalizers are empty.

func NewMutableObjectStore

func NewMutableObjectStore(pool *pgxpool.Pool, schema pkgdb.Schema, table string, opts ...StoreOption) *Store

NewMutableObjectStore constructs a mutable-object Store for tables keyed by namespace/name in schema.

func NewStore

func NewStore(pool *pgxpool.Pool, schema pkgdb.Schema, table string, opts ...StoreOption) *Store

NewStore constructs a tagged-artifact Store bound to a single table (e.g. "agents") in schema. The table must exist; NewStore does not validate it. Queries qualify the table with schema explicitly, so the Store does not depend on the connection's search_path.

For mutable object tables, use NewMutableObjectStore.

func (*Store) ApplyPatch

func (s *Store) ApplyPatch(ctx context.Context, namespace, name, tag string, patch PatchOpts) error

ApplyPatch atomically applies PatchOpts to one row. Tagged-artifact stores require tag=metadata.tag; mutable-object stores ignore tag and use namespace/name. Columns whose mutator is nil are left untouched. Returns pkgdb.ErrNotFound if the row doesn't exist.

Finalizers patching is supported only on the deployments table; the tagged-artifact tables don't carry a finalizers column. Calling PatchFinalizers on a tagged-artifact Store returns an error to surface the misconfiguration loudly rather than silently no-op.

func (*Store) Behavior

func (s *Store) Behavior() StoreBehavior

Behavior reports which private persistence behavior this Store uses. Generic controller/read-model code uses it to interpret ResourceRef tag semantics without maintaining its own per-kind switch.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, namespace, name, tag string) error

Delete removes a single row. Mutable-object stores may use soft-delete plus finalizer drain. Tagged-artifact rows have no finalizers and are hard-deleted immediately so name/tag can be reapplied without waiting for GC. Returns pkgdb.ErrNotFound if the row doesn't exist.

func (*Store) DeleteAllTags

func (s *Store) DeleteAllTags(ctx context.Context, namespace, name string) error

DeleteAllTags hard-deletes every tag row for (namespace, name) on a tagged-artifact table. This is the contract of the batch DELETE endpoint when metadata.tag is omitted; callers delete a single tag by including metadata.tag. Returns pkgdb.ErrNotFound when no row exists for (namespace, name).

Calling on a mutable-object Store is a programming error; the per-kind Store hands mutable objects to the single-row Delete path instead.

func (*Store) DeleteByRef

func (s *Store) DeleteByRef(ctx context.Context, namespace, name, tag string) error

DeleteByRef applies the public reference/delete shape shared by v1alpha1 resources. For tagged artifacts, blank tag deletes every tag for (namespace, name), while a non-empty tag deletes that exact tag. Mutable objects delete by namespace/name and reject explicit tag pins.

func (*Store) FindReferrers

func (s *Store) FindReferrers(ctx context.Context, pathJSON json.RawMessage, opts FindReferrersOpts) ([]*v1alpha1.RawObject, error)

FindReferrers returns rows from this Store's table whose spec JSONB matches pathJSON (via the `@>` containment operator).

func (*Store) Get

func (s *Store) Get(ctx context.Context, namespace, name, tag string) (*v1alpha1.RawObject, error)

Get returns a single row, including terminating rows. For tagged-artifact stores, tag is metadata.tag. Mutable-object stores ignore tag and load by namespace/name. Returns pkgdb.ErrNotFound if missing.

func (*Store) GetByRef

func (s *Store) GetByRef(ctx context.Context, namespace, name, tag string) (*v1alpha1.RawObject, error)

GetByRef resolves the public reference shape shared by v1alpha1 resources. Blank tag means the current live row: literal "latest" for tagged artifacts, namespace/name for mutable objects. Non-empty tag selects a tagged artifact row and is invalid for mutable-object stores.

func (*Store) GetLatest

func (s *Store) GetLatest(ctx context.Context, namespace, name string) (*v1alpha1.RawObject, error)

GetLatest returns the literal "latest" live tag for (namespace, name) on tagged-artifact tables, or the current live row for mutable-object stores. Returns pkgdb.ErrNotFound if no live row exists. Terminating rows are excluded.

func (*Store) GetLatestIncludingTerminating

func (s *Store) GetLatestIncludingTerminating(ctx context.Context, namespace, name string) (*v1alpha1.RawObject, error)

GetLatestIncludingTerminating is GetLatest without the `deletion_timestamp IS NULL` filter, so soft-deleted rows are still returned. Used by resource-handler GET / DELETE paths when the kind opts into IncludeTerminatingByDefault; without this view those handlers contradict LIST, which surfaces the terminating row. Returns pkgdb.ErrNotFound only when no row exists at all.

func (*Store) List

func (s *Store) List(ctx context.Context, opts ListOpts) ([]*v1alpha1.RawObject, string, error)

List returns rows filtered by opts, ordered by stable resource key (namespace, name, tag) with updated_at as a stable tiebreaker. Pagination cursor is returned when more rows are available; pass it back via ListOpts.Cursor to continue. Terminating rows are excluded unless IncludeTerminating is true.

func (*Store) ListTags

func (s *Store) ListTags(ctx context.Context, namespace, name string) ([]*v1alpha1.RawObject, error)

ListTags returns every non-deleted tag row for (namespace, name), ordered by most recently applied first. Tagged-artifact mode only — mutable-object stores do not model "list every tag of a logical resource" and report an error.

Returns an empty slice (no error) when no rows exist for the tag: list semantics differ from the single-row Get path. The HTTP layer surfaces empty results as 200 with `{"items": []}`.

func (*Store) PatchAnnotations

func (s *Store) PatchAnnotations(ctx context.Context, namespace, name, tag string, mutate func(map[string]string) map[string]string) error

PatchAnnotations is a thin wrapper over ApplyPatch for the single- column annotations case.

func (*Store) PatchFinalizers

func (s *Store) PatchFinalizers(ctx context.Context, namespace, name, tag string, mutate func([]string) []string) error

PatchFinalizers is a thin wrapper over ApplyPatch for the single- column finalizers case. Only valid for the deployments table.

func (*Store) PatchStatus

func (s *Store) PatchStatus(ctx context.Context, namespace, name, tag string, mutate func(current json.RawMessage) (json.RawMessage, error)) error

PatchStatus is a thin wrapper over ApplyPatch for the single-column status case.

func (*Store) PurgeFinalized

func (s *Store) PurgeFinalized(ctx context.Context) (int64, error)

PurgeFinalized hard-deletes terminating rows. For deployments this requires finalizers to be empty; for tagged-artifact tables there is no finalizers column, so any row past deletion_timestamp is purged. Returns the number of rows purged.

func (*Store) Upsert

func (s *Store) Upsert(ctx context.Context, obj v1alpha1.Object, opts ...UpsertOpts) (UpsertResult, error)

Upsert applies obj into the Store. Behaviour depends on the table's persistence behavior:

  • Tagged-artifact tables (agents, mcp_servers, etc.) follow declarative tag semantics:
  • missing metadata.tag → default to the literal "latest" tag
  • new (namespace, name, tag) → insert the row
  • same tag and same canonical content hash → no-op
  • same tag and different content hash → replace the row in place
  • Mutable-object tables follow Kubernetes-like update-in-place semantics behind namespace/name key.

Status is never touched by Upsert — use PatchStatus for that.

type StoreBehavior

type StoreBehavior string

StoreBehavior names the private persistence behavior used below the single public v1alpha1 API shape.

const (
	// TaggedArtifactStore keys immutable-ish registry artifacts by
	// namespace/name/tag.
	TaggedArtifactStore StoreBehavior = "TaggedArtifactStore"
	// MutableObjectStore keys normal Kubernetes-like objects by namespace/name
	// in the public API and storage.
	MutableObjectStore StoreBehavior = "MutableObjectStore"
)

type StoreOption

type StoreOption func(*Store)

StoreOption configures an optional Store behaviour at construction time. Options compose; later options override earlier ones for the same field.

func WithAuditor

func WithAuditor(a types.Auditor) StoreOption

WithAuditor plugs a types.Auditor into the Store so every state change the Store considers significant fires the matching audit event after the underlying transaction commits. Default is types.NoopAuditor.

func WithKind

func WithKind(kind string) StoreOption

WithKind tags a Store with the canonical v1alpha1 Kind name (e.g. v1alpha1.KindAgent) so audit events can name the kind without the caller having to set obj.TypeMeta. NewStores sets this for every kind; ad-hoc constructors leave it empty unless the caller passes WithKind explicitly. When unset, the Store falls back to the Kind carried on the inbound object (if any).

type UpsertOpts

type UpsertOpts struct {
	// InitialFinalizers is applied only on the create path for mutable-object
	// stores. Updates preserve existing finalizers.
	InitialFinalizers []string
}

UpsertOpts customizes create-time behavior for Store.Upsert.

type UpsertOutcome

type UpsertOutcome int

UpsertOutcome categorises what an Upsert call did.

const (
	// UpsertCreated reports that a new tag row was inserted.
	UpsertCreated UpsertOutcome = iota
	// UpsertNoOp reports that the incoming content matched the existing row
	// for the tag. No row was written.
	UpsertNoOp
	// UpsertReplaced reports that an existing tag row was atomically replaced
	// with new content.
	UpsertReplaced
)

type UpsertResult

type UpsertResult struct {
	// Tag is the content tag after the call for tagged-artifact tables.
	Tag string
	// UID is the server-managed row identity after the write.
	UID string
	// Generation is the server-managed row generation after the call.
	Generation int64
	// Outcome categorises what the call did. See UpsertOutcome constants.
	Outcome UpsertOutcome
}

UpsertResult is the outcome of Upsert.

Jump to

Keyboard shortcuts

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