Documentation
¶
Overview ¶
Package beehive is an embedded, Kubernetes-inspired control plane backed by a durable store: users declare desired Spec and controllers reconcile actual state toward it, level-triggered, coordinating only through the shared store.
Index ¶
- Constants
- Variables
- func EventDetail[T any](e Event) (T, error)
- type Beehive
- type Change
- type ChangeType
- type Client
- type Condition
- type ConditionStatus
- type Controller
- type ControllerClient
- type Event
- type EventID
- type EventOption
- type EventSpec
- type EventType
- type EventWatcher
- type GroupKind
- type LoadOption
- type LoadSet
- type Migrator
- type Object
- func (o *Object[Spec, Status]) GetOwner() (Ref, bool, error)
- func (o *Object[Spec, Status]) ListDependencies() ([]Ref, error)
- func (o *Object[Spec, Status]) ListDependents() ([]Ref, error)
- func (o *Object[Spec, Status]) ListEvents() ([]Event, error)
- func (o *Object[Spec, Status]) ListOwned() ([]Ref, error)
- type ObjectID
- type Option
- func WithConcurrency(n int) Option
- func WithEventRetention(perObject int, maxAge time.Duration) Option
- func WithFinalizers(f ...string) Option
- func WithLogLevel(level slog.Level) Option
- func WithLogger(l *slog.Logger) Option
- func WithMaxRetryInterval(d time.Duration) Option
- func WithMigrator(m Migrator) Option
- func WithOnCreate(fn func(ctx context.Context)) Option
- func WithOwner(id ObjectID) Option
- func WithResyncInterval(d time.Duration) Option
- func WithSlug(slug string) Option
- func WithStartupReconcileStrategy(s StartupReconcileStrategy) Option
- type RawEvent
- type RawObject
- type Ref
- type Referrer
- type Relation
- type RequeueOption
- type Result
- type Schedule
- type StartupReconcileStrategy
- type Store
- type Watcher
Constants ¶
const ( RelationOwnedBy = storeapi.RelationOwnedBy RelationDependsOn = storeapi.RelationDependsOn )
Variables ¶
var ErrConflictingOption = errors.New("beehive: option conflicts with an explicit argument")
ErrConflictingOption reports an option that contradicts an argument the call already carries — passing WithSlug to a method that takes the slug positionally, for instance. It is distinct from an option being *inapplicable*: an option aimed at a target it doesn't understand is ignored by design (see Option), while a contradiction is a caller mistake whose effect would otherwise be invisible.
var ErrNoController = errors.New("beehive: no controller registered for kind")
ErrNoController is returned by Requeue when the client's kind has no registered controller: there is no reconcile loop to schedule against. A client-only kind is read/write but never reconciled.
var ErrNotFound = storeapi.ErrNotFound
ErrNotFound is returned by Store reads when no object matches.
var ErrNotLoaded = errors.New("beehive: secondary lookup not loaded")
ErrNotLoaded is returned by the secondary-lookup accessors when the requested relation was not fetched on the read that produced the object. It marks caller misuse — forgetting LoadOwner()/LoadDependencies()/LoadDependents() — not a missing object, so it is kept distinct from a present-but-empty result.
var ErrObservedGenerationFuture = storeapi.ErrObservedGenerationFuture
ErrObservedGenerationFuture is returned by UpdateStatus when the caller reports a generation greater than the object's current one — a convergence-handshake violation (a controller must pass the generation it received in Reconcile).
var ErrSchemaVersionDowngrade = storeapi.ErrSchemaVersionDowngrade
ErrSchemaVersionDowngrade is returned by UpdateSpec/UpdateStatus when the caller's schema version is lower than the one stamped on the row — the write-side twin of the read path's refusal to decode data a newer build wrote.
var ErrWrongKind = storeapi.ErrWrongKind
ErrWrongKind is returned by a ControllerClient write when the target id names an object of a different kind than the controller's own. A controller may only write status, conditions, and finalizers on objects of its registered kind; passing an id from another kind (a dependency, an owner) is a bug that would otherwise persist this controller's Status JSON into a foreign row and make later typed reads of that kind fail to decode. The store folds the controller's kind into each write, turning that silent corruption into a loud, retrying reconcile failure. Aliased from storeapi like ErrNotFound.
Functions ¶
func EventDetail ¶ added in v0.11.0
EventDetail unmarshals an event's Detail payload into T. An empty Detail yields the zero value with a nil error; otherwise it returns the result of json.Unmarshal. It is a free generic helper over the non-generic Event, so a single timeline can carry reasons with different detail shapes — decode each with the type its Reason implies.
Types ¶
type Beehive ¶
type Beehive struct {
// contains filtered or unexported fields
}
Beehive is the control plane: it owns the durable store and the set of registered controllers, and drives their reconcile loops between Start and Stop.
func New ¶
New creates a control plane backed by store s. Register controllers on the returned Beehive before calling Start.
func (*Beehive) Start ¶
Start brings the control plane up: it launches the dependency wakers, the per-controller reconcile loops, and the GC sweeper. On success it returns a stop function that tears the control plane back down (see stop). It is an error to start twice or after stop; on error the returned stop is nil. Beehive is a one-shot object: once stopped, create a new instance.
startCtx bounds the startup phase only — a startCtx that's already cancelled aborts startup. The long-lived reconcile loops do not derive from startCtx; the run lifetime ends only when the returned stop is called.
type Change ¶ added in v0.12.0
type Change[Spec, Status any] struct { Type ChangeType Object *Object[Spec, Status] }
Change reports a change to a watched object.
type ChangeType ¶ added in v0.12.0
type ChangeType = storeapi.ChangeType
ChangeType classifies a Change.
type Client ¶
type Client[Spec, Status any] interface { Create(ctx context.Context, spec Spec, opts ...Option) (*Object[Spec, Status], error) CreateOrUpdate(ctx context.Context, slug string, spec Spec) (*Object[Spec, Status], error) // GetOrCreate returns the object with the given slug, creating it from spec if // absent. Unlike CreateOrUpdate it NEVER mutates an existing row: a slug held by // a live OR deletion-pending row is returned as-is with created=false, so the // caller can inspect DeletionRequestedAt and decide whether to wait for GC and // retry — a tombstone still holds the slug's UNIQUE constraint, so no replacement // can be created until GC clears it. The read-or-create is atomic (a single // store.Within transaction), so concurrent reconciles can't both create — one // wins, the other observes it with created=false. // // On create the Added event is emitted and the object enqueued, as with Create; // returning an existing row emits and enqueues nothing. // // opts apply only on the create branch (WithOwner, WithFinalizers, WithOnCreate). // WithSlug is rejected with ErrConflictingOption rather than ignored: the slug is // positional here, so the option can only contradict it, and silently dropping it // would surface much later as an ErrNotFound on the slug the caller meant to use. // // The returned created bool is synchronous, so inside a caller's // ControllerClient.Within it is set before the enclosing transaction commits: a // caller that fires a non-store side effect on created==true acts for a row a // later rollback discards. Route create-conditional side effects through // WithOnCreate, which runs only after the outermost commit; read created as a // correct after-the-fact report, not a pre-commit trigger. // // A non-nil err means nothing was created: the new row is decoded back into // Spec/Status inside the atomic Within, so a spec whose bytes don't round-trip (a // marshal/type bug) rolls the insert back rather than committing a row the process // can't read. Such an error therefore returns created=false, and a retry does not // hit a spurious UNIQUE on a phantom row. (A decode error on the found branch — a // pre-existing poison row this call did not write — likewise returns created=false, // since it plainly wasn't created here.) // // On the found branch the options are ignored outright — an existing row that // lacks the owner or finalizers you passed keeps lacking them, and created=false // is the only signal you get. Do not read created=false as "exists and matches // opts": a caller depending on the owner edge (for cascade collection, say) must // check it with LoadOwner()/GetOwner and reconcile the difference itself. // Beehive can't adopt the row for you — owner is single, so adding the edge to a // row that already has a different owner would produce a two-owner object, and // picking a winner is caller policy. // // spec and opts are nonetheless validated up front, before the slug is read, so // an unmarshalable spec or an erroring option fails the call even when the row // already exists and neither would have been used. Both are caller bugs, and // validating them eagerly keeps the error independent of store state: deferring // would make the same call succeed or fail depending on whether the row happens // to exist, hiding the bug until GC or a cold start removed it. It also keeps // json.Marshal out of the transaction, which on a single-connection store would // hold the write lock across arbitrary user MarshalJSON code. GetOrCreate(ctx context.Context, slug string, spec Spec, opts ...Option) (*Object[Spec, Status], bool, error) Update(ctx context.Context, id ObjectID, spec Spec) (*Object[Spec, Status], error) Get(ctx context.Context, id ObjectID, loads ...LoadOption) (*Object[Spec, Status], error) GetBySlug(ctx context.Context, slug string, loads ...LoadOption) (*Object[Spec, Status], error) List(ctx context.Context, loads ...LoadOption) ([]*Object[Spec, Status], error) // Delete soft-deletes the object (sets DeletionRequestedAt) and advances GC so // the controller runs its finalizers; physical removal follows once they clear. // An id naming no object of this kind is ErrNotFound — contrast DeleteBySlug, // which folds absence to nil. Delete(ctx context.Context, id ObjectID) error // DeleteBySlug requests deletion of the object with the given slug. It is // idempotent: a slug that matches no object returns nil (already gone), and a // row already deletion-pending is a no-op returning nil (as Delete is on a // repeated call). Kind-scoped like GetBySlug — a slug is per-kind, so this only // ever targets this client's kind. Deletion itself is Delete's: a soft delete // plus a GC advance. // // The delete-if-present partner to GetOrCreate's create-if-absent, so an // ensure/remove pair is one call on each side. DeleteBySlug(ctx context.Context, slug string) error Watch(ctx context.Context, id ObjectID) (<-chan Change[Spec, Status], error) WatchList(ctx context.Context) (<-chan Change[Spec, Status], error) // GetOwner returns id's owner, if any. ok reports presence: false (with a nil // error) when the object simply has no owner. The lazy counterpart to // LoadOwner() — fetch the owner only when it is actually needed. // // This and the three ListDependencies/ListDependents/ListOwned lookups read // their edge query directly and do not kind-scope id: passing another kind's // id reads that kind's edges, and a missing id reads empty — neither reports // ErrNotFound. Reserve them for ids this client owns. GetOwner(ctx context.Context, id ObjectID) (Ref, bool, error) // ListDependencies returns the objects id depends on (its outgoing depends_on // edges). The lazy counterpart to LoadDependencies(). ListDependencies(ctx context.Context, id ObjectID) ([]Ref, error) // ListDependents returns the objects that depend on id (incoming depends_on). // The lazy counterpart to LoadDependents(). ListDependents(ctx context.Context, id ObjectID) ([]Ref, error) // ListOwned returns the objects id owns (its incoming owned_by edges). The // lazy counterpart to LoadOwned(). ListOwned(ctx context.Context, id ObjectID) ([]Ref, error) // ListOwnedObjects returns the objects owned by ownerID that belong to THIS // client's kind, fully decoded — the typed, kind-scoped form of ListOwned // (which returns untyped Refs across every owned kind, leaving the caller to // filter by Kind and Get each child through that kind's client). Ownership is // the owned_by edge (child -> owner), so these are ownerID's children of this // kind, ordered by id as ListOwned is. // // ownerID need not be this client's kind — it is the owner, typically another // kind — and like ListOwned it is not kind-scoped or existence-checked: an // owner with no children of this kind, and an ownerID that doesn't exist, both // read empty rather than ErrNotFound. A deletion-pending child is included; // whether to skip it is the caller's call (check DeletionRequestedAt). // Undecodable rows are quarantined and logged, as in List. // // It takes the same LoadOptions as List, batched the same way: without them the // children come back with nothing loaded and their ref/event accessors return // ErrNotLoaded. Pass e.g. LoadOwned() to walk a second level of the tree // without a Get per child — the per-child Get this method exists to avoid. ListOwnedObjects(ctx context.Context, ownerID ObjectID, loads ...LoadOption) ([]*Object[Spec, Status], error) // ListEvents returns id's event-log runs, newest-first, filtered by the given // options (see EventOption). Like the ref lookups it reads by id and does not // kind-scope: a foreign id reads that object's log. An empty log is an empty slice. ListEvents(ctx context.Context, id ObjectID, opts ...EventOption) ([]Event, error) // GetLatestEvent returns the current (most-recent) run in id's category timeline. // ok reports presence: false (with a nil error) when the timeline is empty. GetLatestEvent(ctx context.Context, id ObjectID, category string) (Event, bool, error) // WatchEvents streams id's event log: the runs matching opts as a snapshot, then // live runs, on the returned channel. The channel closes when ctx is cancelled or // the stream ends. Like Watch it requires a registered controller and is scoped to // this client's kind. Runs conflate per run, so a lagging reader converges to each // run's latest state. WatchEvents(ctx context.Context, id ObjectID, opts ...EventOption) (<-chan Event, error) // Requeue requeues id for immediate reconcile. A latency hint, not a // synchronous run; correctness rests on the periodic resync, not this. // // By default it preserves id's retry backoff ladder: a requeue is the common // event-driven nudge (config change, dependency update, manual poke) and // almost never proves the failure condition is resolved. The ladder is cleared // by a successful reconcile or by passing WithResetBackoff(), never by a plain // requeue. Pass WithResetBackoff() only when the caller knows the failure is // resolved and the next retry should start from the base interval. // // Returns ErrNotFound if id does not exist and ErrNoController if the kind has // no registered controller. Requeue(ctx context.Context, id ObjectID, opts ...RequeueOption) error // GetSchedule reports id's Schedule: when the reconcile loop has, in advance, // scheduled id to be requeued — a pending backoff retry or RequeueAfter delay, // or now if it is already queued — in Schedule.NextRequeueAt, or the zero time // there when nothing is scheduled. The Schedule wrapper leaves room for future // fields (e.g. a reschedule trigger) without a breaking change. // // This is a non-blocking, best-effort read of in-memory schedule state — it // touches no store, so the error is reserved for symmetry and never returned // today. An id that does not exist (or belongs to another kind) is simply // unscheduled, and a client-only kind has no reconcile loop at all; both read as // the zero-value Schedule, indistinguishable from a real object with nothing // scheduled. // // This is the next *scheduled* requeue, not a prediction of the next reconcile. // It does not — and cannot — account for wakes that aren't a per-id timer: the // periodic resync (kind-wide, conditional on being unsettled), dependency-change // wakes, store-write enqueues, or Requeue. So the actual next reconcile may be // earlier than reported, and a zero NextRequeueAt means "nothing scheduled", not // "will not reconcile". Treat it as observability, not a guarantee. Use // WatchSchedule to observe changes live. GetSchedule(ctx context.Context, id ObjectID) (Schedule, error) // WatchSchedule streams id's schedule as a gauge: the current value on subscribe, // then a new Schedule on every (re)schedule — backoff step, RequeueAfter, resync // or dependency wake, dispatch, or Requeue — none of which the object Watch sees. // The channel closes when ctx is cancelled or the control plane stops. A lagging // reader converges to the latest value (per-id coalescing), so it can miss // intermediate values but never the current one. Unlike GetSchedule, a client-only // kind returns ErrNoController rather than hang on a stream that can never emit; id // need not exist — an unscheduled id streams the zero Schedule until scheduled. WatchSchedule(ctx context.Context, id ObjectID) (<-chan Schedule, error) }
Client is the user-facing API for a single resource kind: the surface for creating, reading, updating, deleting, and watching objects.
type Condition ¶
type Condition struct {
Type string
Status ConditionStatus
Reason string
Message string
// Liveness marks a condition derived from a live in-process resource: it is
// valid only within the writing process. The store downgrades a liveness
// condition written by a prior process to Unknown ("verifying") until a
// controller re-confirms it. The default (false) is durable store-truth.
Liveness bool
}
Condition is a standard observation about an object's state, reported by its controller (e.g. type "Ready", status True).
type ConditionStatus ¶
type ConditionStatus string
ConditionStatus is the state of a Condition: True, False, or Unknown.
const ( ConditionTrue ConditionStatus = "True" ConditionFalse ConditionStatus = "False" ConditionUnknown ConditionStatus = "Unknown" )
type Controller ¶
type Controller[Spec, Status any] interface { Reconcile(ctx context.Context, client ControllerClient[Status], obj *Object[Spec, Status]) (Result, error) }
Controller is the user-supplied reconcile logic for a resource kind. Reconcile is called to drive an object toward its desired state; the client is the status-write surface for this controller's kind. Controllers own no lifecycle in beehive — any background work belongs to the embedding application, which obtains a ControllerClient from Register.
type ControllerClient ¶
type ControllerClient[Status any] interface { // UpdateStatus records status and the generation this reconcile observed. // Status that marshals to the stored bytes writes nothing: no // resource_version bump and no Modified event, so a controller can report // unconditionally without waking watchers (or dependents) on an unchanged // poll. The exception is the generation handshake — if this reconcile // settled a generation the object hadn't settled at before, that advance is // recorded and does emit, so watchers see the object converge. UpdateStatus(ctx context.Context, id ObjectID, observedGeneration int64, status Status) error SetCondition(ctx context.Context, id ObjectID, condition Condition) error DeleteCondition(ctx context.Context, id ObjectID, conditionType string) error // RecordEvent appends an observation to id's event log, aggregating into // contiguous runs (see EventSpec). Like the other writes it is kind-folded and // composes in Within, so a controller can record an event and update a // condition atomically. RecordEvent(ctx context.Context, id ObjectID, event EventSpec) error DeleteFinalizer(ctx context.Context, id ObjectID, finalizer string) error AddDependency(ctx context.Context, fromID, toID ObjectID) error DeleteDependency(ctx context.Context, fromID, toID ObjectID) error // HasIncomingRefs reports whether any object with a live claim still points at id: // an owned child, or a dependent that is not itself being deleted. A dependent // that is itself finalizing is excluded — it's going away and no longer has a // claim. A finalizer can gate teardown on this: a controller holding a shared // resource clears its finalizer only once nothing with a live claim references // the object, so the resource outlives its last real user. HasIncomingRefs(ctx context.Context, id ObjectID) (bool, error) // GetOwner returns id's owner, if any (its outgoing owned_by edge). ok reports // presence: false with a nil error when the object has no owner. The lazy // counterpart to a reconciler's LoadOwner default. GetOwner(ctx context.Context, id ObjectID) (Ref, bool, error) // ListDependencies returns the objects id depends on (outgoing depends_on). ListDependencies(ctx context.Context, id ObjectID) ([]Ref, error) // ListDependents returns the objects that depend on id (incoming depends_on). ListDependents(ctx context.Context, id ObjectID) ([]Ref, error) // ListOwned returns the objects id owns (its incoming owned_by edges). ListOwned(ctx context.Context, id ObjectID) ([]Ref, error) // Within runs fn inside a single transaction: the ControllerClient writes fn // makes (with the ctx passed to it) all commit together on a nil return, or all // roll back on error. Reconcile itself is not transactional — each write // otherwise commits on its own — so a controller uses Within only for the // writes that must be atomic. The transaction holds the store's write lock for // fn's whole duration, so keep external I/O outside it. Within(ctx context.Context, fn func(ctx context.Context) error) error }
ControllerClient is the write surface a controller uses to report observed state. It only writes Status and metadata — never Spec, which the user owns.
func Register ¶
func Register[Spec, Status any](bh *Beehive, gk GroupKind, c Controller[Spec, Status], opts ...Option) (ControllerClient[Status], error)
Register installs controller c for the resource kind gk and returns the kind's ControllerClient — the status-write surface, which the embedding application can inject into the controller and use from its own goroutines. It must be called before Start, and only once per kind. On any error it returns (nil, err).
type Event ¶ added in v0.11.0
type Event struct {
ID EventID
ObjectID ObjectID // object this event is about
Category string
Type EventType
Reason string
Message string // latest occurrence's message
Detail json.RawMessage // latest occurrence's payload; nil = none
Count int // occurrences in this run (>= 1)
FirstAt time.Time // run start
LastAt time.Time // run end (latest occurrence)
}
Event is one contiguous run of observations about an object, aggregated by (Category, Type, Reason). Count grows and the [FirstAt, LastAt] window widens while the run holds; a change in the key starts a new run.
type EventOption ¶ added in v0.11.0
type EventOption func(*storeapi.EventQuery)
EventOption filters a Client.ListEvents / WatchEvents read. Like LoadOption and RequeueOption it is distinct from Option: it applies only to the event reads, composing into the store's event query.
func WithEventCategory ¶ added in v0.11.0
func WithEventCategory(category string) EventOption
WithEventCategory restricts a read to a single timeline. The category "" is the default timeline (distinct from "no filter", which is the absence of this option).
func WithEventLimit ¶ added in v0.11.0
func WithEventLimit(n int) EventOption
WithEventLimit caps a read to the newest n runs. It bounds only the snapshot of a WatchEvents subscription, not its live stream.
func WithEventReason ¶ added in v0.11.0
func WithEventReason(reason string) EventOption
WithEventReason restricts a read to runs with the given reason.
func WithEventType ¶ added in v0.11.0
func WithEventType(t EventType) EventOption
WithEventType restricts a read to one severity (Normal or Warning).
func WithEventsSince ¶ added in v0.11.0
func WithEventsSince(t time.Time) EventOption
WithEventsSince restricts a read to runs still active at or after t (LastAt >= t).
type EventSpec ¶ added in v0.11.0
type EventSpec struct {
Category string // independent timeline; "" = default
Type EventType
Reason string // machine-readable token, e.g. "ProbeFailed"
Message string // human-readable; sampled, not keyed
Detail any // optional payload; marshaled on write; nil = none
}
EventSpec is the caller-supplied portion of an event, passed to ControllerClient.RecordEvent. It excludes the store-owned run fields (id, count, window) so a caller can't set them. Consecutive emissions sharing (Category, Type, Reason) coalesce into one run; Message and Detail are sampled (latest wins), not part of that key.
type EventType ¶ added in v0.11.0
type EventType string
EventType classifies an event's severity: Normal (✓) or Warning (✗).
type EventWatcher ¶ added in v0.11.0
type EventWatcher = storeapi.EventWatcher
EventWatcher is a subscription to one object's event log, returned by the store's WatchEvents. The client decodes its raw runs into public Events.
type LoadOption ¶ added in v0.4.0
type LoadOption func(*LoadSet)
LoadOption selects a secondary lookup to fetch alongside an object on a read. It is distinct from Option: it applies only to read call sites (Get/GetBySlug/ List), composing into a LoadSet. Lazy fetching is the alternative — omit the selector and call Client.GetOwner/ListDependencies when the data is needed.
func LoadDependencies ¶ added in v0.4.0
func LoadDependencies() LoadOption
LoadDependencies selects the objects this one depends on (outgoing depends_on).
func LoadDependents ¶ added in v0.4.0
func LoadDependents() LoadOption
LoadDependents selects the objects that depend on this one (incoming depends_on).
func LoadEvents ¶ added in v0.11.0
func LoadEvents() LoadOption
LoadEvents selects the object's event-log runs, read via Object.ListEvents(). It loads the object's current runs (bounded by retention); for filtered or bounded reads use the lazy Client.ListEvents with EventOptions instead.
func LoadOwned ¶ added in v0.5.0
func LoadOwned() LoadOption
LoadOwned selects the objects this one owns (its incoming owned_by edges).
func LoadOwner ¶ added in v0.4.0
func LoadOwner() LoadOption
LoadOwner selects the object's owner (its outgoing owned_by edge).
type LoadSet ¶ added in v0.4.0
type LoadSet uint8
LoadSet is a bitset of secondary lookups (owner, dependencies, dependents, owned) to fetch alongside an object. The zero value loads nothing; reads OR in the bits a caller selects, and the populated Object records what was fetched so the accessors can tell "loaded and empty" from "never asked".
const ( // LoadOwnerBit selects the object's owner (its outgoing owned_by edge). LoadOwnerBit LoadSet = 1 << iota // LoadDependenciesBit selects the object's dependencies (outgoing depends_on). LoadDependenciesBit // LoadDependentsBit selects the objects that depend on it (incoming depends_on). LoadDependentsBit // LoadOwnedBit selects the objects this one owns (incoming owned_by edges). LoadOwnedBit // LoadEventsBit selects the object's event-log runs (see LoadEvents). LoadEventsBit )
type Migrator ¶ added in v0.3.0
type Migrator interface {
// SchemaVersionSpec is the spec schema version this build writes (and
// converts up to). 0 means spec is not versioned for this kind.
SchemaVersionSpec() int
// SchemaVersionStatus is the status schema version this build writes (and
// converts up to). 0 means status is not versioned for this kind.
SchemaVersionStatus() int
// ConvertSpec upgrades spec bytes written at version from to the current spec
// version. It is called only when 0 <= from < SchemaVersionSpec(); from == 0 is
// the unversioned baseline (a row written before this kind opted into
// versioning), so once a migrator is enabled the converter must handle it.
ConvertSpec(from int, raw json.RawMessage) (json.RawMessage, error)
// ConvertStatus upgrades status bytes written at version from to the current
// status version. It is called only when 0 <= from < SchemaVersionStatus();
// from == 0 is the unversioned baseline the converter must handle (see
// ConvertSpec).
ConvertStatus(from int, raw json.RawMessage) (json.RawMessage, error)
}
Migrator upgrades a kind's stored Spec/Status JSON to the shape this build expects, at the rawToTyped decode boundary. Beehive stores Spec and Status as opaque JSON and records the schema version each blob was written at (per column); on read, a blob older than this build's current version is converted up before it is unmarshalled, so a consumer can evolve its Spec/Status structs without breaking decode of rows written by an earlier build.
Spec and Status carry independent versions and convert independently — a status-only write re-stamps only the status version. A current version of 0 means "not versioned": that blob is never converted (the kind hasn't opted in for it). Conversion is lazy: bytes are upgraded on read and re-stamped only when the blob is next written, never by a bulk row rewrite.
Register a Migrator per kind via WithMigrator passed to Register.
type Object ¶
type Object[Spec, Status any] struct { ID ObjectID Group string Kind string Slug *string Spec Spec Status *Status Generation int64 // bumped on every Spec write not provably a no-op (see UpdateSpec) ObservedGeneration *int64 // Generation the controller last reconciled; nil until first reconcile ObservedAt *time.Time // when ObservedGeneration was recorded; not a reconcile heartbeat ResourceVersion int64 // bumped on every write, for optimistic concurrency DeletionRequestedAt *time.Time // set when deletion is requested; object lingers until finalizers clear Finalizers []string Conditions []Condition // per-type observations reported by controllers CreatedAt time.Time UpdatedAt time.Time // contains filtered or unexported fields }
Object is a single resource: user-owned desired state (Spec) plus controller-owned observed state (Status), along with the metadata Beehive uses to track convergence and deletion.
func (*Object[Spec, Status]) GetOwner ¶ added in v0.4.0
GetOwner returns the object's owner. It errors with ErrNotLoaded if LoadOwner() was not passed to the read. Otherwise ok reports presence — false when the object has no owner. (Use the lazy Client.GetOwner to fetch on demand instead.)
func (*Object[Spec, Status]) ListDependencies ¶ added in v0.6.0
ListDependencies returns the objects this one depends on, or ErrNotLoaded if LoadDependencies() was not passed to the read. A loaded-but-empty result is an empty slice with a nil error.
func (*Object[Spec, Status]) ListDependents ¶ added in v0.6.0
ListDependents returns the objects that depend on this one, or ErrNotLoaded if LoadDependents() was not passed to the read.
func (*Object[Spec, Status]) ListEvents ¶ added in v0.11.0
ListEvents returns the object's event-log runs, newest-first, or ErrNotLoaded if LoadEvents() was not passed to the read. A loaded-but-empty log is an empty slice with a nil error. (Use the lazy Client.ListEvents to fetch on demand, or to filter/limit.)
type Option ¶
Option configures a target — a Beehive, a reconciler, or a per-object operation — depending on where it is passed. Each option type-switches on the targets it understands and ignores the rest.
func WithConcurrency ¶
WithConcurrency sets the number of concurrent worker goroutines for a controller. When passed to New it becomes the default for all controllers; when passed to Register it overrides that default for a single controller. A value <= 1 means single-threaded (the default).
func WithEventRetention ¶ added in v0.11.0
WithEventRetention bounds the per-object event log, enforced globally by the GC sweeper on the startup + resync cadence. perObject > 0 caps each (object, category) timeline to its newest perObject runs — a ring, so a flapping timeline can't evict a quiet one; maxAge > 0 drops runs whose window ended more than maxAge ago. A zero bound is skipped, and both zero (the default) leaves the log unbounded. Retention is global, so it is meaningful only at New; passed elsewhere it is ignored.
func WithFinalizers ¶
WithFinalizers attaches finalizers that must be cleared before an object is physically deleted.
func WithLogLevel ¶
WithLogLevel sets the minimum level beehive emits, layered on top of whatever the logger's own handler already filters. It lets callers quiet beehive down without building a leveled handler; pass a very high level to silence it while keeping the logger wired up. Has no effect without WithLogger (the discard logger emits nothing regardless).
Passed to New it applies to the control plane and is the default for all controllers; passed to Register it overrides that default for one controller.
func WithLogger ¶
WithLogger routes beehive's internal logging through l. Pass a logger whose slog.Handler wraps your logging library — zap, zerolog, logrus, and logr all ship slog bridges — to forward beehive's logs into it. A nil logger disables logging entirely, which is the default.
Passed to New it sets the logger for the control plane and the default for all controllers; passed to Register it overrides that default for one controller.
func WithMaxRetryInterval ¶
WithMaxRetryInterval caps the exponential backoff between failed reconciles for a controller (the default is defaultMaxRetryInterval). A value <= 0 is ignored, keeping the default: a zero or negative cap would clamp every retry delay to it and busy-loop the reconciler the instant it keeps returning an error, which is never what a caller wants.
func WithMigrator ¶ added in v0.3.0
WithMigrator registers a Migrator for the controller's kind, supplying the schema-version conversion applied to stored Spec/Status JSON on read (see Migrator). It is meaningful only at Register — a migrator is per-kind, and Register installs it into the shared registry that both the user-facing client and the reconciler decode through. Passed anywhere else it is ignored.
func WithOnCreate ¶ added in v0.17.0
WithOnCreate registers fn to run once, on the caller's ctx, only if this call actually inserts a new row — and only after the outermost transaction it runs in commits. Create always fires it; GetOrCreate fires it on the create branch but not when it returns an existing row.
It is the commit-safe channel for create-conditional side effects. The GetOrCreate created bool is synchronous, so inside a caller's ControllerClient.Within it reports true before the enclosing transaction commits — act on it for a non-store side effect (an external call, an in-memory counter) and a later rollback leaves that effect fired for a row that never landed. fn is deferred through the same post-commit path as the reconcile wake (Store.AfterCommit), so a rollback simply never runs it. Put such side effects here rather than gating them on the returned bool.
func WithResyncInterval ¶
WithResyncInterval sets the periodic resync interval for a controller. A value <= 0 disables periodic resync, leaving the controller event-driven only.
func WithStartupReconcileStrategy ¶
func WithStartupReconcileStrategy(s StartupReconcileStrategy) Option
WithStartupReconcileStrategy sets which objects a controller reconciles at startup (see StartupReconcileStrategy). The default is StartupReconcileAll. Passed to New it sets the default for all controllers; passed to Register it overrides that default for one.
type RawEvent ¶ added in v0.11.0
RawEvent is the untyped event-log row below the generic boundary — one aggregated run. The client decodes it into the public Event.
type RawObject ¶
RawObject is the untyped row below the generic boundary. Spec and Status are opaque JSON bytes; everything else is Beehive-owned metadata that mirrors the objects table. The reconciler and client decode Spec/Status into typed Object[Spec, Status] values; the Store never inspects them.
type Ref ¶ added in v0.4.0
Ref identifies a related object reached through a ref edge — an owner, a dependency, or a dependent — carrying the GroupKind needed to address it. It is the same shape the store returns for every edge query.
type Referrer ¶
Referrer is an object pointing at a target through a ref edge, with the GroupKind needed to route a requeue.
type RequeueOption ¶ added in v0.8.0
type RequeueOption func(*requeueOptions)
RequeueOption configures a Client.Requeue call. Like LoadOption it is distinct from Option: it applies only to Requeue.
func WithResetBackoff ¶ added in v0.8.0
func WithResetBackoff() RequeueOption
WithResetBackoff makes a Requeue clear the object's retry backoff ladder so its next failure retries from the base interval. Pass it only when the caller has proof the failure condition is resolved. A plain Requeue preserves the ladder: backoff is cleared by a successful reconcile or an explicit WithResetBackoff, never by merely being asked to try again.
type Result ¶
type Result struct {
// RequeueAfter requeues the object after the given delay. Zero means no
// explicit requeue (the object still resyncs on the periodic timer).
RequeueAfter time.Duration
}
Result is returned by a controller's Reconcile to influence requeueing.
type Schedule ¶ added in v0.13.0
type Schedule struct {
// NextRequeueAt is when the reconcile loop has scheduled the object to be
// requeued, or the zero time when nothing is scheduled. It reflects only per-id
// timers (backoff, RequeueAfter, an immediate enqueue), not the periodic resync
// or event-driven wakes. Reported by Client.GetSchedule and WatchSchedule.
NextRequeueAt time.Time
}
Schedule reports when an object is next due to reconcile. It is a struct rather than a bare time.Time so fields can be added without a breaking change — a reschedule watcher (Client.WatchSchedule) observes this value as a gauge.
type StartupReconcileStrategy ¶
type StartupReconcileStrategy int
StartupReconcileStrategy selects which objects a controller reconciles once at startup. The zero value is StartupReconcileAll, so the safe default holds for a controller that never sets it.
const ( // StartupReconcileAll reconciles every object at startup, settled or not. The // full pass re-confirms process-scoped state such as liveness conditions, // which read as "verifying" after a restart until a controller rewrites them. StartupReconcileAll StartupReconcileStrategy = iota // StartupReconcileUnsettled reconciles only objects whose spec has not yet // converged — cheaper, but leaves process-scoped state unconfirmed until some // other event wakes the object. StartupReconcileUnsettled // StartupReconcileNone does no startup reconcile at all, leaving the periodic // resync (and live events) as the only drivers. StartupReconcileNone )
type Store ¶
Store is the durable-store contract Beehive depends on internally. It is non-generic and deals only in raw rows: the generic-to-non-generic boundary lives one layer up, in the typedController adapter.
Mutators return the freshly written row so callers see the store-assigned id, resource_version, and timestamps without a re-read.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
storeapi
Package storeapi defines the storage contract shared between the beehive control plane and its store implementations (e.g.
|
Package storeapi defines the storage contract shared between the beehive control plane and its store implementations (e.g. |
|
Package sqlite provides a durable, SQLite-backed implementation of the beehive Store, including the conflating watch fan-out and schema migrations.
|
Package sqlite provides a durable, SQLite-backed implementation of the beehive Store, including the conflating watch fan-out and schema migrations. |
|
Package sqlitemigrate is a tiny, forward-only SQL migration runner for SQLite databases.
|
Package sqlitemigrate is a tiny, forward-only SQL migration runner for SQLite databases. |