resource

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package resource is the agent's unified foundation: everything in the system is a Resource, a typed, versioned, schema-validated record on one event-sourced store. One model, one envelope, one scope/label system, one provenance path.

The shape is deliberately Kubernetes-aligned (APIVersion + Kind + Spec/Status + Labels/Selectors), because that declarative model is proven and instantly familiar, but these are Flynn resources, not Kubernetes objects: they carry our own API groups (e.g. core.ionagent.io/v1alpha1), live in this package, and are never the upstream k8s types (which belong to the Kubernetes integration). The alignment keeps a future where resources export as CRDs / GitOps-manage open, without depending on a cluster.

Every kind is data, including Kind itself (a Kind is a Resource of kind "Kind"), so the agent can author and validate new kinds at runtime. Typed domains layer on top as thin facades; specialized indexes (full-text, vector, ordering) are projections over the resource event log, not parallel stores.

Index

Constants

View Source
const (
	EvPut     = "resource.put"
	EvDeleted = "resource.deleted"
	// EvMerged records a resource applied by cross-instance Merge: its payload is
	// the winning post-image verbatim (the remote envelope preserved, not
	// restamped), so every replica that folds the stream converges identically. It
	// projects exactly like EvPut, but is a distinct type so the log stays auditable
	// about which records arrived by local write versus fleet sync.
	EvMerged = "resource.merged"
)

Resource event types: the vocabulary of the resource stream. Each event is the canonical post-image of the affected resource, so replaying in order reproduces identical state. Exported so durable backends project the same vocabulary.

View Source
const CoreGroupVersion = "core.ionagent.io/v1alpha1"

CoreGroupVersion is the API group/version for the foundation's own built-in kinds. Domains define their own groups (e.g. skill.ionagent.io/v1); the `.ionagent.io` suffix marks every kind unmistakably as ours, never a Kubernetes built-in.

View Source
const KindKind = "Kind"

KindKind is the name of the kind that describes kinds. A Kind is itself stored as a Resource of this kind, which is what makes the type system data the agent can author and validate at runtime (meta-circular self-extension).

View Source
const ResourceStream = "resources"

ResourceStream is the spine stream every resource mutation is recorded on. One ordered stream means a Replay folds the whole foundation back into existence.

Variables

View Source
var ErrConflict = errors.New("resource: version conflict")

ErrConflict is returned when optimistic concurrency fails: the caller passed a non-zero SyncVersion that no longer matches the stored record. Re-read and retry.

View Source
var ErrInvalid = errors.New("resource: invalid")

ErrInvalid is returned when a resource fails admission (missing required field, unregistered kind, or a Spec that does not satisfy its kind's JSON schema).

View Source
var ErrNotFound = errors.New("resource: not found")

ErrNotFound is returned when a requested resource does not exist.

View Source
var ErrSkipUpdate = errors.New("resource: skip update")

ErrSkipUpdate is returned by an Update mutate function to end the retry loop without writing: the state the mutation observed makes the write unnecessary (a park already cleared, a limit already exceeded). Update returns the resource as read and a nil error.

Functions

func DecodeSpec

func DecodeSpec[T any](r Resource) (T, error)

DecodeSpec reads a resource's typed spec. An empty spec decodes to the zero value, so a kind whose spec is optional reads cleanly without a nil check.

func DecodeStatus

func DecodeStatus[T any](r Resource) (T, error)

DecodeStatus reads a resource's typed status. An empty status decodes to the zero value: a freshly created resource has no status yet.

func EncodeStatus

func EncodeStatus[T any](s T) (json.RawMessage, error)

EncodeStatus marshals a typed status for writing back onto a resource.

func Hash

func Hash(r Resource) (string, error)

Hash returns the stable content hash of r: a hex SHA-256 over its canonical content (identity, scope, labels and annotations, spec, status, valid-time, and the tombstone flag), excluding the volatile envelope fields (versions, clocks, timestamps, and the hash itself). Equal content yields an equal hash on any machine, so resource history forms a Merkle DAG: dedup, provenance ("which version produced this"), tamper-evidence, and efficient diff-based sync.

Spec is canonicalized (re-encoded with sorted keys) so two semantically equal specs that differ only in key order or whitespace hash the same. Status is folded in as a fixed-size digest of its bytes rather than canonicalized: status is self-produced by one owning controller through a stable Go encoder (so byte identity already equals semantic identity), and Merge decides conflicts by envelope while carrying the origin's hash verbatim, so it never re-hashes a remote status. Digesting it keeps a write allocation-flat no matter how large the status grows (a goal embeds its whole transcript in status), instead of materializing the entire status as a generic tree on every write.

func Hashes

func Hashes(r Resource) (content, spec string, err error)

Hashes returns Hash and SpecHash together, canonicalizing the spec once. The Stamper stamps both onto every write, so readers (the reconciler's no-op check above all) compare stored fields instead of re-canonicalizing per tick.

func MarshalSnapshot

func MarshalSnapshot(resources []Resource, lastSeq int64) ([]byte, error)

MarshalSnapshot serializes a projection (all its records, live and tombstoned, and the Seq it is current as of) into the snapshot payload Replay restores from. A backend builds the record set from its own storage and calls this, so every backend snapshots in one identical format. Records are sorted by id so the payload is deterministic.

func MergeEvent

func MergeEvent(r Resource) (spine.AppendInput, error)

MergeEvent builds the spine event recording that r was applied by cross-instance merge. Its payload is the winning post-image verbatim, so folding the stream reproduces the merged state on any backend, and it preserves r's provenance (origin instance and writer actor) rather than restamping it locally.

func OwnerGone

func OwnerGone(ctx context.Context, store Store, r Resource) (bool, error)

OwnerGone reports whether r's controller owner no longer exists or is itself terminating, which makes r an orphan a garbage collector should reap so an owner's deletion cascades to the subtree it created. A resource with no controller owner is a root and is never orphaned. The owner is resolved by its stable id, so a rename never breaks the link. It is the reusable predicate a kind's reconciler calls to garbage-collect owned resources.

func RegisterCoreKinds

func RegisterCoreKinds(reg *Registry) error

RegisterCoreKinds registers the foundation's built-in kinds, starting with Kind itself (kind == "Kind"), so a Kind can be stored and validated as a Resource. This bootstraps meta-circularity: the type system is data on the same store.

func SpecHash

func SpecHash(r Resource) (string, error)

SpecHash returns a stable hash of a resource's desired state alone (its kind and canonical spec), excluding status and all envelope metadata. It is the agent's equivalent of Kubernetes' metadata.generation: a controller records the SpecHash it last acted on in status, and a reconcile is a no-op while the stored spec hash still matches, so writing status (which changes the full content hash) never re-triggers work. Equal spec yields an equal hash on any machine.

func ValidateForMerge

func ValidateForMerge(r Resource) error

ValidateForMerge checks that a replicated record carries the full envelope Merge relies on. A record produced by a real write always has these; rejecting a record without them guards against feeding a half-built local resource into the merge path (where no Stamper assigns identity). Backends call it before resolving.

Types

type AnyScopeGetter

type AnyScopeGetter interface {
	GetAnyScope(ctx context.Context, kind, name string) (Resource, bool, error)
}

AnyScopeGetter is an optional Store capability: a keyed lookup of the first live resource of a kind with a name in any scope, in the same scope-then-name order ListAll surfaces. A caller resolving a scope-independent name (the control plane's cross-scope get, when the global scope missed) reads through it when the backend offers it, so the lookup seeks to the name instead of listing and scanning the whole kind. found is false with a nil error when no scope has the name. A backend that cannot answer this from an index simply does not implement it; the caller falls back to ListAll and a name scan.

type Condition

type Condition struct {
	Type               string    `json:"type"`
	Status             string    `json:"status"` // "True" | "False" | "Unknown"
	Reason             string    `json:"reason,omitempty"`
	Message            string    `json:"message,omitempty"`
	LastTransitionTime time.Time `json:"lastTransitionTime"`
}

Condition is one standard status condition, the Kubernetes-shaped signal a controller writes to say where a resource stands (Ready, Stalled, ...). It is shared so every kind reports conditions in one shape and a generic reader (a CLI listing, a wait-for-ready helper) can interpret any kind's status.

func SetCondition

func SetCondition(conds []Condition, c Condition, now time.Time) []Condition

SetCondition upserts c by type and returns the updated slice, stamping LastTransitionTime only when the status value actually changes, so a no-op reconcile does not churn the time.

type Envelope

type Envelope struct {

	// The shared sync envelope: SyncVersion (optimistic concurrency),
	// OriginInstanceID (creator provenance), UpdatedHLC + LastWriterID (the
	// last-writer-wins merge key), and Deleted (the syncing tombstone). The
	// fields and the stamping rules over them are defined once in the envelope
	// package and shared with state records, so fleet merge is one discipline,
	// not two. Embedding inlines the fields, so the wire format and hashes are
	// unchanged.
	envelope.Envelope
	// WriterActor records who authored the last write: a human, the agent, or the
	// runtime (system). It is the provenance signal cross-instance Merge uses for
	// precedence, so a person's correction outranks a later automated write and is
	// never silently overwritten by the fleet. It is metadata, not content, so it is
	// excluded from the content hash (identical content authored by different actors
	// shares a hash). The zero value is treated as the agent.
	WriterActor spine.ActorType

	// Finalizers are keys that must be cleared before the resource is actually
	// removed. While any remain, a Delete does not tombstone the record: it sets
	// DeletionTimestamp and the resource stays live (still returned by reads) so
	// each owner can run its cleanup and then remove its own key. When the last
	// finalizer is removed from a resource that has a DeletionTimestamp, the delete
	// completes and the record tombstones. This is how external state (a worktree, a
	// child run) is cleaned up reliably, even across a crash: a controller re-reads
	// the pending deletion every reconcile until cleanup is done.
	Finalizers []string
	// DeletionTimestamp is set when a delete is requested on a resource that still
	// has finalizers: the resource is terminating but not yet gone. Nil means the
	// resource is not being deleted. It is system-assigned, never set or cleared by
	// a Put (only Delete sets it; resurrecting a tombstone clears it), so a caller
	// cannot fake or cancel a deletion by writing the field.
	DeletionTimestamp *time.Time
	// OwnerReferences link this resource to the resources that own it (its parent
	// run or goal). The controller owner (Controller=true) drives its lifecycle: a
	// garbage collector reaps the resource once that owner is gone or terminating,
	// so deleting an owner cascades to the subtree it created. They are metadata,
	// excluded from the content hash. A resource with none is a root, owned by
	// nothing, which is the single-run (n=1) case.
	OwnerReferences []OwnerReference

	// Version is the content revision (incremented on every content change),
	// distinct from SyncVersion (the sync/concurrency token).
	Version int64
	// ContentHash is a stable hash of the resource's canonical content (see
	// Hash). Equal content yields an equal hash across machines, which makes
	// history a Merkle DAG: dedup, "which version produced this", tamper-evidence,
	// and efficient diff-based sync.
	ContentHash string
	// SpecHash is a stable hash of the resource's desired state alone (see
	// SpecHash), stamped at write time so a controller's no-op check ("has the
	// spec I acted on changed?") reads a field instead of re-canonicalizing the
	// spec every reconcile tick.
	SpecHash string

	// ValidFrom and ValidTo are the resource's valid-time: when it became and
	// ceased to be true in the world, distinct from event-time (when we recorded
	// it, carried by UpdatedHLC and the event log). Nil ValidFrom means "valid
	// since creation"; nil ValidTo means "still valid". Reserved from day one so a
	// second time axis never requires a schema migration; the query surface that
	// uses them can follow.
	ValidFrom *time.Time
	ValidTo   *time.Time

	CreatedAt time.Time
	UpdatedAt time.Time
}

Envelope is the universal metadata carried by every resource: sync/concurrency, content provenance, and bitemporal time. Designing the whole envelope in from the first write keeps replay, optimistic concurrency, fleet merge, verifiable history, and as-of queries reachable without ever migrating it in later.

type Key

type Key struct {
	Kind  string
	Scope Scope
	Name  string
}

Key uniquely identifies a resource by its logical coordinates (Kind, Scope, Name), the address an upsert or fetch targets. ID addresses the same record by its stable id. Key is comparable.

type KeyLister

type KeyLister interface {
	ListKeys(ctx context.Context, kind string) ([]Key, error)
}

KeyLister is an optional Store capability: a key-only listing of every live resource of a kind, ordered by scope then name. Callers that need addresses rather than records (the reconcile resync, which only enqueues keys) read through it when the backend offers it, so a periodic sweep does not copy every record of the kind. Both bundled backends implement it.

type Kind

type Kind struct {
	APIVersion string          // group/version of resources of this kind, e.g. "skill.ionagent.io/v1"
	Name       string          // e.g. "Skill"
	Schema     json.RawMessage // JSON Schema for Spec; nil = unconstrained
	Singular   string          // optional display form
	Plural     string          // optional display form
}

Kind describes a resource kind: the API group/version its resources carry, the kind name, and the JSON Schema their Spec must satisfy (admission). A nil Schema means specs are unconstrained.

type MergeOutcome

type MergeOutcome int

MergeOutcome reports what a Merge did to the local record.

const (
	// MergeApplied means the remote record won and replaced (or first-inserted) the
	// local one.
	MergeApplied MergeOutcome = iota
	// MergeIgnored means the local record won and the remote was discarded as older
	// or lower-precedence. The local state is unchanged.
	MergeIgnored
	// MergeUnchanged means the remote was already present (same write), so the apply
	// was an idempotent no-op.
	MergeUnchanged
)

func (MergeOutcome) String

func (o MergeOutcome) String() string

type MergeResult

type MergeResult struct {
	Outcome  MergeOutcome
	Resource Resource
}

MergeResult is the outcome of applying a replicated resource: what happened and the resource that now holds the record's slot (the winner, or the unchanged local record).

type Op

type Op int

Op is a label-selector match operator.

const (
	// OpExists matches when the key is present (any value).
	OpExists Op = iota
	// OpNotExists matches when the key is absent.
	OpNotExists
	// OpEquals matches when the key's value equals the requirement value.
	OpEquals
	// OpNotEquals matches when the key is absent or its value differs.
	OpNotEquals
	// OpIn matches when the key's value is one of the requirement values.
	OpIn
	// OpNotIn matches when the key is absent or its value is none of the values.
	OpNotIn
)

type Option

type Option func(*memStore)

Option configures the in-memory Store.

func WithClock

func WithClock(c clock.Clock) Option

WithClock sets the time source (default clock.System), so tests and replay can supply a clock.Manual.

func WithEventLog

func WithEventLog(l spine.Log) Option

WithEventLog backs the store with a specific spine.Log instead of a private in-memory one (inject a shared log to observe, audit, or Replay the stream).

func WithIDGenerator

func WithIDGenerator(g *ids.Generator) Option

WithIDGenerator sets the source of resource IDs (default: a generator on the store's clock with crypto/rand entropy). Supply a seeded generator for deterministic replay.

func WithInstanceID

func WithInstanceID(id string) Option

WithInstanceID sets the origin/last-writer instance stamped onto records this store creates (default "local").

func WithSnapshotCodec

func WithSnapshotCodec(c spine.SnapshotCodec) Option

WithSnapshotCodec makes the store's snapshots verified: Snapshot seals the projection payload through the codec before saving it, and Replay opens (verifies) a stored snapshot through it before restoring - one that fails to open is skipped and the stream is folded from the start instead. With a codec set, an unsigned or tampered snapshot is never restored.

func WithSnapshotEvery

func WithSnapshotEvery(k int) Option

WithSnapshotEvery makes the store checkpoint itself automatically: after every k successful mutations it writes a snapshot, so a later Replay folds at most k events past the last checkpoint instead of the whole stream. The snapshot is written after the mutation completes, outside any lock, and best effort: a snapshot failure never fails the write (a missing snapshot is only slower, never wrong). Zero or negative disables automatic snapshots (the default).

type OwnerReference

type OwnerReference struct {
	// APIVersion and Kind identify the owner's kind.
	APIVersion string
	Kind       string
	// Name and ID address the owner. ID is the stable, unambiguous handle the
	// collector resolves the owner by; Name is the logical handle for selectors and
	// debugging.
	Name string
	ID   string
	// Controller marks the one owner that manages this resource's lifecycle. A
	// resource has at most one controller owner; when that owner is gone or
	// terminating, the resource is garbage-collected.
	Controller bool
}

OwnerReference records that a resource is owned by another resource: the parent run or goal that created it. It is the graph edge the garbage collector follows to reap a resource once its owner is gone, so deleting an owner cascades to the subtree it created. Owner references are envelope metadata, excluded from the content hash.

type Registry

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

Registry holds the registered kinds and validates resources against them. It is safe for concurrent use, and is the admission control point: a resource of an unregistered kind, or one whose Spec fails its kind's schema, is rejected before it is ever stored.

func NewRegistry

func NewRegistry(opts ...RegistryOption) *Registry

NewRegistry returns an empty registry using the built-in schema compiler unless one is injected.

func (*Registry) Kinds

func (r *Registry) Kinds() []Kind

Kinds returns every registered kind, ordered by group/version then name, for introspection ("what can this agent represent?").

func (*Registry) Lookup

func (r *Registry) Lookup(apiVersion, name string) (Kind, bool)

Lookup returns the registered kind for (apiVersion, name).

func (*Registry) Register

func (r *Registry) Register(k Kind) error

Register adds (or replaces) a kind, compiling its JSON Schema up front so an invalid schema is rejected at registration rather than at first write.

func (*Registry) Validate

func (r *Registry) Validate(apiVersion, kind string, spec []byte) error

Validate is admission: the kind must be registered, and spec must satisfy its JSON Schema (if the kind declares one). Returns an ErrInvalid-wrapped error otherwise.

type RegistryOption

type RegistryOption func(*Registry)

RegistryOption configures a Registry.

func WithSchemaCompiler

func WithSchemaCompiler(c SchemaCompiler) RegistryOption

WithSchemaCompiler overrides the schema compiler (default: the built-in, dependency-free subset compiler). A host can inject a full JSON Schema engine.

type Requirement

type Requirement struct {
	Key    string
	Op     Op
	Values []string
}

Requirement is one label constraint. Values is empty for existence operators, a single element for (in)equality, and a set for In/NotIn.

type Resource

type Resource struct {
	APIVersion string
	Kind       string
	ID         string
	Name       string
	// GenerateName requests a server-assigned Name for a kind that has no natural
	// one. When Name is empty and GenerateName is set, a create assigns
	// Name = GenerateName + ID (e.g. "mem-01J..."); it is a create-only directive,
	// consumed and cleared on write, never stored. Setting Name takes precedence
	// and GenerateName is ignored. Mirrors Kubernetes metadata.generateName, but
	// uses our globally unique, sortable ID as the suffix instead of a random one,
	// so there is never a name collision to retry.
	GenerateName string
	Scope        Scope
	Labels       map[string]string
	Annotations  map[string]string
	// Spec is the desired state (validated against the kind's JSON schema). It is
	// raw JSON so it embeds readably in events and hashes canonically; nil when
	// unset.
	Spec json.RawMessage
	// Status is the observed state (set by controllers/reconcilers, not admitted
	// against the spec schema). Raw JSON; nil when unset.
	Status json.RawMessage
	Envelope
}

Resource is the universal record for every data-defined kind in the system.

Identity: ID is a stable, globally unique id assigned on creation. Name is the logical name, unique within (Kind, Scope), the handle callers use to upsert and fetch (a Skill's slug, an Agent's name). APIVersion is the kind's group/version, e.g. "skill.ionagent.io/v1".

Kinds without a natural name (a memory fact, a turn, a run) set GenerateName instead of Name: the store assigns Name = GenerateName + ID on create, so every such record gets a unique, sortable name from the one deterministic ID source rather than each facade minting its own. See GenerateName.

func DecodeResource

func DecodeResource(payload map[string]any) (Resource, error)

DecodeResource reconstructs a Resource from an event payload. Durable backends use it to project the same records the in-memory core does.

func KindResource

func KindResource(k Kind, scope Scope) (Resource, error)

KindResource renders a Kind as a Resource of kind "Kind", so kind definitions are stored and synced through the same foundation as everything else. Optional fields are omitted when empty so the spec satisfies the Kind schema.

func Resolve

func Resolve(incoming, current Resource) (winner Resource, takeIncoming bool)

Resolve decides a cross-instance conflict between an incoming replicated record and the current local one for the same ID, returning the winner and whether it is the incoming record. It is the pure heart of Merge, deterministic and commutative so every instance converges on the same result no matter the order records arrive. The rules, in order:

  1. Idempotence: the same write (equal UpdatedHLC and LastWriterID) is a no-op, so re-delivering a record never changes anything.
  2. Provenance precedence: a human-authored write outranks an automated one regardless of clocks, because human intent should not be silently overwritten by a later agent or system write.
  3. Last-writer-wins by UpdatedHLC, with LastWriterID as the deterministic tiebreak when two writes share an exact HLC.

Tombstones carry the same envelope as live writes, so a delete and a write are ordered by the very same key: a higher-HLC delete removes, and a higher-HLC write after a delete intentionally resurrects.

func UnmarshalSnapshot

func UnmarshalSnapshot(payload []byte) ([]Resource, int64, error)

UnmarshalSnapshot decodes a snapshot payload back into the records and the Seq it is current as of - the inverse of MarshalSnapshot, so any backend restores from the one shared format.

func Update

func Update(ctx context.Context, store Store, kind string, scope Scope, name string, mutate func(*Resource) error) (Resource, error)

Update applies mutate to the resource addressed by (kind, scope, name) under optimistic concurrency: read, mutate, Put, and on ErrConflict re-read and reapply against the fresh version. It is the one conflict-retry policy for read-modify-write updates, so every caller converges under contention the same way instead of hand-rolling its own bound and yield. mutate sees a fresh copy each attempt and must be safe to run more than once. A read error, a mutate error other than ErrSkipUpdate, and a non-conflict write error end the loop and are returned as-is (a caller maps ErrNotFound to its own semantics). An exhausted retry budget returns an error matching errors.Is ErrConflict.

func UpdateByID

func UpdateByID(ctx context.Context, store Store, id string, mutate func(*Resource) error) (Resource, error)

UpdateByID is Update addressed by the resource's stable id.

func (Resource) Controller

func (r Resource) Controller() (OwnerReference, bool)

Controller returns the resource's controller owner reference (the owner that manages its lifecycle) and whether one is set. At most one owner reference is the controller.

func (Resource) Key

func (r Resource) Key() Key

Key returns the resource's logical key.

func (Resource) ValidAt

func (r Resource) ValidAt(t time.Time) bool

ValidAt reports whether the resource is true in the world at valid-time t.

The valid interval is half-open, [from, to): the instant a fact ceases to be true is excluded, so adjacent intervals tile without overlap. A nil ValidFrom defaults to the resource's creation time (valid-time tracks event-time when the writer did not say otherwise); a nil ValidTo means the fact is still valid, with no upper bound.

type SchemaCompiler

type SchemaCompiler interface {
	Compile(schema []byte) (Validator, error)
}

SchemaCompiler compiles a JSON Schema document (raw JSON) into a reusable Validator. It is a port: the foundation ships a small, zero-dependency compiler (newBuiltinCompiler) that supports the structural subset of JSON Schema our specs use; a host that needs full JSON Schema 2020-12 compliance can inject an adapter wrapping a complete engine, without the core depending on it.

func NewSchemaCompiler

func NewSchemaCompiler() SchemaCompiler

NewSchemaCompiler returns the built-in, dependency-free schema compiler, so a caller outside this package can validate an instance against a JSON Schema with the same engine the resource store uses. The semantics match registered-kind validation exactly.

type Scope

type Scope struct {
	Instance  string
	Project   string
	Workspace string
}

Scope locates a resource on the instance/project/workspace axis (the namespace), so resources can be partitioned and resolved most-specific-first and shared selectively across a fleet. The zero Scope is the global (instance) scope. Scope is comparable.

type Selector

type Selector []Requirement

Selector is a conjunction of requirements: a resource matches when its labels satisfy every requirement. The empty Selector matches everything. This is the universal declarative query plane over all kinds (the Kubernetes label-selector model).

func Everything

func Everything() Selector

Everything is the selector that matches every resource.

func ParseSelector

func ParseSelector(s string) (Selector, error)

ParseSelector parses a Kubernetes-style label selector: comma-separated requirements, each one of `key`, `!key`, `key=value`, `key==value`, `key!=value`, `key in (a, b)`, or `key notin (a, b)`. An empty string is Everything.

func (Selector) Matches

func (s Selector) Matches(labels map[string]string) bool

Matches reports whether the labels satisfy all requirements.

type Stamper

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

Stamper computes the canonical post-image of a resource mutation: it runs admission (the kind must be registered and the Spec must satisfy its schema), assigns identity, the sync envelope, content and sync versions, the content hash, and timestamps, enforces optimistic concurrency, and builds the spine event to append. Every backend routes writes through one Stamper, so the rules live in exactly one place and backends cannot drift. It does no persistence: the caller supplies the existing record (looked up under its own lock/transaction) and persists the returned resource and event.

func NewStamper

func NewStamper(instanceID string, clk clock.Clock, hc *hlc.Clock, gen *ids.Generator, reg *Registry) *Stamper

NewStamper builds a Stamper from a backend's instance identity, deterministic primitives, and the kind registry used for admission.

func (*Stamper) Delete

func (s *Stamper) Delete(r Resource) (Resource, spine.AppendInput, error)

Delete requests deletion of the given live resource. If it has no finalizers it tombstones immediately (EvDeleted). If it has finalizers it is marked terminating instead: DeletionTimestamp is set and the record stays live (EvPut) so its owners can run cleanup and remove their finalizer keys; the deletion completes later, in Put, when the last finalizer is removed.

func (*Stamper) Put

func (s *Stamper) Put(existing *Resource, r Resource) (Resource, spine.AppendInput, error)

Put stamps a create or update of r keyed by (Kind, Scope, Name). existing is the stored record for that key (tombstones included, so a put over a tombstone resurrects it) or nil. It admits the spec, enforces opt-in CAS, and recomputes the content hash, returning the canonical resource and the event to append.

func (*Stamper) Registry

func (s *Stamper) Registry() *Registry

Registry returns the registry this stamper admits against.

type Store

type Store interface {
	// Put creates or updates the resource addressed by (Kind, Scope, Name). It
	// validates Spec against the kind's schema (admission), assigns identity,
	// envelope, content hash, and timestamps, and records the mutation on the
	// event log. Optimistic concurrency is opt-in via SyncVersion.
	Put(ctx context.Context, r Resource) (Resource, error)
	// Get returns the live resource for (kind, scope, name), or ErrNotFound.
	Get(ctx context.Context, kind string, scope Scope, name string) (Resource, error)
	// GetByID returns the live resource by its stable id, or ErrNotFound.
	GetByID(ctx context.Context, id string) (Resource, error)
	// List returns the live resources of a kind in a scope whose labels satisfy
	// the selector (nil selector matches all), ordered by name.
	List(ctx context.Context, kind string, scope Scope, sel Selector) ([]Resource, error)
	// ListAll returns the live resources of a kind across every scope whose labels
	// satisfy the selector (nil selector matches all), ordered by scope then name.
	// It is the cross-namespace query: typed facades that resolve by a
	// scope-independent handle (a skill slug, say) and selector-driven views over a
	// whole kind read through it, the way Kubernetes lists a kind across all
	// namespaces.
	ListAll(ctx context.Context, kind string, sel Selector) ([]Resource, error)
	// Delete requests deletion of the resource addressed by (kind, scope, name), or
	// returns ErrNotFound. With no finalizers it tombstones immediately; with
	// finalizers it marks the resource terminating (sets DeletionTimestamp, keeps it
	// live) and the deletion completes via Put when the last finalizer is removed.
	// Deleting an already-terminating resource is an idempotent no-op.
	Delete(ctx context.Context, kind string, scope Scope, name string) error
	// Merge applies a resource replicated from another instance, converging the two
	// without losing a write. Distinct from Put (the local-write command): Merge
	// trusts the remote envelope (ID, origin, HLC, versions, provenance) and never
	// restamps it, so all replicas reach byte-identical state regardless of the
	// order replicas arrive. See Resolve for the conflict rules; the result reports
	// whether the remote was applied, ignored as stale, or already present.
	//
	// Identity is the global ID: a record is merged against the local record with
	// the same ID. The same (Kind, Scope, Name) created independently on two
	// instances has two different IDs and so stays two distinct records; resolving
	// such a name collision is a higher-level concern, not part of the apply path.
	Merge(ctx context.Context, remote Resource) (MergeResult, error)
	// Snapshot checkpoints the current projection onto the event log, so a later
	// rebuild resumes from the snapshot and folds only the events after it instead
	// of replaying the whole stream. A snapshot is a derived cache: it never changes
	// what a read returns, only how fast a rebuild is.
	Snapshot(ctx context.Context) error
	// Close releases backend resources.
	Close() error
}

Store is the generic, event-sourced port every kind is read and written through: the single interface a backend implements to persist the entire foundation. Adding a domain is registering a Kind, never editing this interface or forking a backend. Backends (in-memory, SQLite, a host) are interchangeable, held to one contract by resourcetest.RunSuite.

func NewMemory

func NewMemory(reg *Registry, opts ...Option) Store

NewMemory returns an in-memory Store admitting against reg. Every mutation is recorded on a spine.Log and projected, so the store's state is always a fold of its log (see Replay). Safe for concurrent use; the zero-setup default backend.

func Replay

func Replay(ctx context.Context, log spine.Log, reg *Registry, opts ...Option) (Store, error)

Replay reconstructs an in-memory Store by folding a log's resource stream: the running proof that the resource layer is a projection of the spine. It resumes from the log's latest resource snapshot when one exists and folds only the events after it, so a long-lived stream rebuilds in bounded work instead of from the start. The result is identical either way: a snapshot is a cache, not a source of truth.

type Validator

type Validator interface {
	Validate(instance any) error
}

Validator checks a decoded JSON instance against a compiled schema, returning an error describing the first violation.

Directories

Path Synopsis
Package resourcetest is the conformance suite for resource.Store.
Package resourcetest is the conformance suite for resource.Store.

Jump to

Keyboard shortcuts

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