component

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package component defines the extension contract for the unbounded operator. A component is a unit of desired state the operator reconciles for Sites. Implementations satisfy either ClusterComponent (cluster-wide singletons such as net and machina) or SiteComponent (per-Site units such as metalman) and are assembled into a Registry the SiteReconciler drives. Adding a component is a matter of implementing one interface and registering it; the reconcile loop, status conditions, ordering, and the Site-less pass are all driven from the registry.

Index

Constants

View Source
const (
	// FieldOwner is the server-side apply field manager the operator uses for
	// every object it applies.
	FieldOwner = "unbounded-operator"

	// CRDKind is the Kind of a CustomResourceDefinition object in the embedded
	// manifests. The operator installs CRDs once at startup, so components skip
	// them during reconcile.
	CRDKind = "CustomResourceDefinition"

	// NamespaceKind is the Kind of a Namespace object in the embedded
	// component manifests. Components never write it; see NamespaceOperation.
	NamespaceKind = "Namespace"

	// SiteLabelKey is the canonical node label for site membership
	// (unbounded-cloud.io/site). Per-site components node-select on it.
	SiteLabelKey = unboundedv1alpha3.MachineSiteLabelKey

	// DeprecatedSiteLabelKey is the node site-membership label used by released
	// net controllers before the switch to unbounded-cloud.io/site. Per-site
	// workloads target either key during the deprecation window so they schedule
	// before the upgraded net has converged all Nodes to the canonical label.
	DeprecatedSiteLabelKey = "net.unbounded-cloud.io/site"

	// AppliedHashLabel records the exact SSA payload last submitted by the
	// operator. A matching cached object can skip an identical apply without
	// confusing API defaulting or server-managed fields with desired drift.
	AppliedHashLabel = "unbounded-cloud.io/applied-hash"

	// SingletonRequestName cannot collide with a valid Site name. Managed
	// singleton resource events use it independently of Site fan-out.
	SingletonRequestName = "__singletons"

	// BuildDefaultNamespace is the namespace baked into the embedded component
	// manifests at build time. RetargetNamespace rewrites it to the operator's
	// configured namespace when they differ.
	BuildDefaultNamespace = unbounded.DefaultSystemNamespace
)
View Source
const (
	ReasonReconciled     = "Reconciled"
	ReasonDisabled       = "Disabled"
	ReasonNoSites        = "NoSites"
	ReasonReconcileError = "ReconcileError"

	// ReasonBackendNotReady reports that a component withheld objects which
	// direct apiserver traffic at a backend of its own, because that backend is
	// not serving.
	//
	// It covers both ways of arriving there. Usually the backend is known to be
	// down, the component is otherwise reconciled, and what is missing is state
	// it does not control and can only wait for. It is also used when the
	// backend's state could not be read at all: the component still emits
	// everything the answer does not govern, but sets Err, so the pass is a
	// reconcile error and goes through backoff. See NotReadyErr.
	ReasonBackendNotReady = "BackendNotReady"

	// ReasonDependencyNotWritten marks a component whose operations were never
	// attempted because something they depend on failed. It is deliberately
	// distinct from ReconcileError: the component itself did not fail, and the
	// component that did already reports the underlying error.
	ReasonDependencyNotWritten = "DependencyNotWritten"

	// ReasonOverrideNotApplied marks a component whose workload was withheld
	// because the overrides that would have shaped it could not be used. The
	// running workload is untouched, which is the point, but the component is
	// not reconciled: it did not write what it planned.
	ReasonOverrideNotApplied = "OverrideNotApplied"

	// ReasonPlanRejected marks every component in a pass the executor refused
	// to run at all, for a dependency cycle or contradictory shared
	// operations. Nothing was written, so no component's planning verdict
	// describes the cluster.
	ReasonPlanRejected = "PlanRejected"
)

Reason strings published on Site.status.conditions. They must be CamelCase alphanumeric to satisfy metav1.Condition validation.

View Source
const NamespaceOwner = "unbounded-operator"

NamespaceOwner names the operator as the component that owns the namespace. It is not a registered component, so it never appears as a Site condition; it exists to attribute the operation in plan summaries and execution results.

Variables

View Source
var DefaultNamespace = unbounded.SystemNamespace()

DefaultNamespace is the namespace the operator installs components into. It follows the operator's own namespace (unbounded.SystemNamespace()).

Functions

func AppliedPayloadHash added in v0.9.0

func AppliedPayloadHash(obj *unstructured.Unstructured) (string, error)

AppliedPayloadHash returns the label-safe digest used to identify an exact desired SSA payload. Unpadded Base32 preserves all 256 bits in 52 alphanumeric characters, including the first and last characters required by label values.

func ConfigMapPayloadChanged

func ConfigMapPayloadChanged(oldConfig, newConfig *corev1.ConfigMap) bool

ConfigMapPayloadChanged reports whether two ConfigMaps differ in payload.

func ConfigMapPayloadHash

func ConfigMapPayloadHash(config *corev1.ConfigMap) string

ConfigMapPayloadHash hashes the complete ConfigMap payload. JSON provides a deterministic encoding for string-keyed maps, including binary values.

func DesiredFieldsMatch added in v0.9.0

func DesiredFieldsMatch(desired, current any) bool

DesiredFieldsMatch reports whether every field declared by desired has the same value in current. Extra current fields are ignored because they may be API defaults or fields owned by users and other controllers.

func IsRBACObject

func IsRBACObject(obj *unstructured.Unstructured, nameContains string) bool

IsRBACObject reports whether obj is an RBAC object (ServiceAccount, Role, RoleBinding, ClusterRole, or ClusterRoleBinding) whose name contains nameContains. It single-sources the shared decision of which RBAC objects in a bundled manifest set belong to a given component, so a cluster component that skips another component's RBAC and the per-Site component that applies it cannot drift apart.

func SetNamedContainerImage added in v0.2.0

func SetNamedContainerImage(obj *unstructured.Unstructured, name, image string) error

SetNamedContainerImage sets the image of only the container named name in a workload pod template, searching both initContainers and containers. Unlike SetPodSpecImages it leaves every other container untouched, so a workload that mixes an operator-managed image with pinned helper images (for example a component whose pod also runs a busybox init) keeps those helper images.

func SetPodSpecImages

func SetPodSpecImages(obj *unstructured.Unstructured, image string) error

SetPodSpecImages replaces every init and main container image in a workload.

func SiteNodeAffinity

func SiteNodeAffinity(siteName string) *corev1.Affinity

SiteNodeAffinity matches Nodes carrying either the canonical site label or the deprecated net-prefixed site label. The OR is required during migration.

func SiteOwnerReference

func SiteOwnerReference(site *unboundedv1alpha3.Site) metav1.OwnerReference

SiteOwnerReference builds the controller owner reference used for per-site resources. Using owner references rather than a Site finalizer lets Kubernetes garbage collect per-site workloads if a Site is deleted while the operator is down.

Controller is set so controller-runtime's Owns() watch (which enqueues only via metav1.GetControllerOf) re-reconciles the Site when an owned resource drifts or is deleted. BlockOwnerDeletion is intentionally left unset: setting it triggers the OwnerReferencesPermissionEnforcement admission check for update on sites/finalizers, which the operator ServiceAccount does not hold, and would cause every owned-object apply to be rejected.

func ToUnstructured

func ToUnstructured(obj client.Object) *unstructured.Unstructured

ToUnstructured converts a client.Object into its unstructured form.

func UpsertOwnerReference

func UpsertOwnerReference(refs []metav1.OwnerReference, owner metav1.OwnerReference) ([]metav1.OwnerReference, bool)

UpsertOwnerReference adds or updates owner in refs, returning whether the slice changed. An existing reference to the same owner (matched by UID, Kind, and APIVersion) is rewritten when its Name or Controller flag differs, so a resource adopted before Controller ownership was introduced converges to a controller reference on the next reconcile.

func YamlFiles

func YamlFiles(fsys fs.FS) ([]string, error)

YamlFiles returns the sorted list of YAML files in a manifest filesystem.

Types

type ClusterComponent

type ClusterComponent interface {
	// Name is the stable, unique component identifier (for example "net"). It is
	// used in logs and must be unique within a Registry.
	Name() string

	// ConditionType is the Site status condition type published for this
	// component (for example "NetReady"). It must be CamelCase alphanumeric and
	// unique within a Registry.
	ConditionType() string

	// Plan computes the operations that would drive the component to its
	// desired state given every Site, and reports the planning verdict.
	//
	// Planning may read cluster state, because decisions like "does this
	// ConfigMap already exist" and "is this singleton retained" depend on it.
	// Planning must not write: that is the executor's job, and it is what lets
	// a pass validate everything before anything is written.
	//
	// The Result carries verdicts only planning can reach, such as Disabled or
	// NoSites. The driver folds execution outcomes into it.
	Plan(ctx context.Context, env *Env, sites []unboundedv1alpha3.Site) (*Plan, Result, error)
}

ClusterComponent is a cluster-wide unit of desired state (for example net or machina). It reconciles once per pass from the full set of Sites and is never tied to a single Site, so it also runs on the Site-less pass (a Site deletion or a managed singleton-resource event).

Plan runs on every pass and must be idempotent. A not-ready Result does not by itself schedule a retry: the driver requeues on Result.Err or after Result.RequeueAfter, otherwise the component only re-runs when one of the watches it registers via WatchProvider fires. On the Site-less pass there is no Site status to publish, so a not-ready Result there is meaningful only through its Err or RequeueAfter.

type Config

type Config struct {
	// ImageRegistry is the full image-repository prefix (registry host plus any
	// org/namespace path) that operator-managed component images live under. The
	// component's flat repository name and the operator's image tag are appended
	// to it, so ImageRegistry=ghcr.io/azure yields ghcr.io/azure/machina:<tag>.
	// It carries no implicit path segment: forks and mirrors set it to their own
	// prefix (for example ghcr.io/myorg or registry.corp.internal/unbounded).
	ImageRegistry string
	ImageTag      string

	// APIServerEndpoint is injected into the machina controller config and
	// advertised to metalman.
	APIServerEndpoint string
}

Config carries operator-level settings components read while reconciling.

func (Config) Image

func (c Config) Image(repository string) string

Image returns the operator-managed image for repository. ImageRegistry is a full repository prefix, so repository is appended directly with no implicit org/namespace segment.

type Env

type Env struct {
	Client    client.Client
	Scheme    *runtime.Scheme
	Namespace string
	Config    Config

	// APIReader reads straight from the apiserver, bypassing the manager
	// cache.
	//
	// Planning may need to know whether a workload the operator applied is
	// actually running, which the cache answers badly for two reasons. It is
	// populated by a watch, so it lags the apiserver by however long an event
	// takes to arrive, and a gate reading a stale Deployment reports the
	// outgoing revision's availability as the current one's. And every kind
	// read through it establishes an informer, so answering whether one
	// Service has a backend would cache every Endpoints, EndpointSlice, Pod
	// and ReplicaSet in the namespace for the life of the process.
	//
	// It may be nil in unit tests that do not need it; LiveReader falls back to
	// the cached client so a component never has to nil-check it.
	APIReader client.Reader
}

Env is the shared execution context handed to every component. It bundles the Kubernetes client, scheme, target namespace, and operator Config together with the manifest-apply and helper machinery components use to reconcile.

func (*Env) ApplyObject

func (e *Env) ApplyObject(ctx context.Context, obj client.Object) error

ApplyObject server-side applies a single object with the operator field owner. It skips the write when the cached object carries the hash of the exact same desired payload. Read failures are deliberately treated as cache misses so reconciliation still attempts the authoritative write and surfaces its error.

func (*Env) DecodeManifestFS added in v0.3.0

func (e *Env) DecodeManifestFS(manifests fs.FS, mutate func(*unstructured.Unstructured) error) ([]*unstructured.Unstructured, error)

DecodeManifestFS decodes every YAML object in the manifest filesystem, running mutate on each decoded object and then retargeting its namespace. A mutate that nils out obj.Object drops that object.

It performs no writes. Components use it while planning, so a pass can decide everything it intends to do before anything is written.

func (*Env) DecodeManifestFiles added in v0.3.0

func (e *Env) DecodeManifestFiles(manifests fs.FS, files []string, mutate func(*unstructured.Unstructured) error) ([]*unstructured.Unstructured, error)

DecodeManifestFiles decodes a specific list of YAML manifest files. Callers use it for a curated subset, for example excluding an examples/ subtree that YamlFiles would otherwise include.

func (*Env) DefaultConfigMap

func (e *Env) DefaultConfigMap(manifests fs.FS, name, component string) (*corev1.ConfigMap, error)

DefaultConfigMap extracts the named ConfigMap from a manifest filesystem and retargets it to the operator namespace. component names the manifest set for error messages.

func (*Env) DeleteIfExists

func (e *Env) DeleteIfExists(ctx context.Context, obj client.Object) error

DeleteIfExists deletes an object, treating an already-absent object as success.

func (*Env) Execute added in v0.3.0

func (e *Env) Execute(ctx context.Context, plan *Plan) (ExecutionResult, error)

Execute runs a plan.

Execution is explicitly not transactional. Kubernetes offers no multi-object transaction and no rollback, so the guarantees are narrower and stated here: operations run in dependency order; a failure does not abort the pass, so independent operations still execute; the transitive dependents of a failure are skipped rather than attempted; nothing already written is undone; and every outcome is attributed to the component and Site that planned it.

Because every operation is idempotent, a partially failed pass is recoverable by re-planning and re-executing rather than by resuming from a checkpoint.

func (*Env) InNamespaceNamed

func (e *Env) InNamespaceNamed(names ...string) func(client.Object) bool

InNamespaceNamed returns a matcher for objects in the operator namespace whose name is one of names.

func (*Env) InNamespaceWithPrefix

func (e *Env) InNamespaceWithPrefix(prefix string) func(client.Object) bool

InNamespaceWithPrefix returns a matcher for objects in the operator namespace whose name carries prefix followed by a non-empty suffix (a per-site config).

func (*Env) ListSites

func (e *Env) ListSites(ctx context.Context) ([]unboundedv1alpha3.Site, error)

ListSites returns every Site in the cluster.

This is an unscoped list, and it has to be: every pass fans out over the result, and a component decides whether it is enabled by inspecting it.

It is correct only because Site is cluster-scoped. The operator runs with its cache scoped to its own namespace, and controller-runtime's multi-namespace cache routes cluster-scoped kinds to a separate cluster-wide cache. Were Site ever made namespaced, this same call would return only the Sites in the operator's namespace and would report no error at all: every other Site would silently lose its per-Site workloads, and the cluster components would conclude they were disabled. TestSiteMustBeClusterScopedForTheScopedCache pins that invariant to this call site.

func (*Env) LiveReader added in v0.3.0

func (e *Env) LiveReader() client.Reader

LiveReader returns the reader to use for state a pass is about to make a decision on, where a cached answer that is merely a few seconds old is the wrong answer.

See Env.APIReader for why the cache is the wrong source for those reads. The fallback exists so tests that construct an Env directly keep working; a running operator always sets APIReader from mgr.GetAPIReader().

func (*Env) ManagedConfigPredicate

func (e *Env) ManagedConfigPredicate(match func(client.Object) bool) predicate.Predicate

ManagedConfigPredicate fires for create and delete of ConfigMaps that match, and for updates only when the ConfigMap payload actually changed. It filters out status/metadata-only churn so a component is not re-applied on every event.

func (*Env) ManagedWorkloadPredicate

func (e *Env) ManagedWorkloadPredicate(match func(client.Object) bool) predicate.Predicate

ManagedWorkloadPredicate fires for create and delete of workloads that match, and for updates when the generation or the operator's own metadata changed.

func (*Env) OwnedWorkloadPredicate

func (e *Env) OwnedWorkloadPredicate() predicate.Predicate

OwnedWorkloadPredicate fires for create and delete of owned workloads, and for updates when the generation or the operator's own metadata changed. It carries no name match because the Owns() enqueue handler already scopes events to resources owned by the Site; the predicate exists only to drop status-only churn (pod counts) so drift and deletion self-heal without re-applying on every status update. See workloadChanged.

func (*Env) RequestSingleton

func (e *Env) RequestSingleton() handler.EventHandler

RequestSingleton enqueues the synthetic singleton request. Cluster components use it to map events on their managed singleton workloads back to a reconcile that reconciles cluster-wide state without any specific Site.

func (*Env) RequestSiteFromConfigName

func (e *Env) RequestSiteFromConfigName(prefix string) handler.EventHandler

RequestSiteFromConfigName maps a per-site ConfigMap named "<prefix><site>" back to a reconcile request for that Site. Names without the prefix, or with an empty site suffix, are ignored.

func (*Env) RetargetNamespace

func (e *Env) RetargetNamespace(obj *unstructured.Unstructured)

RetargetNamespace rewrites embedded-manifest references from the build-time default namespace (BuildDefaultNamespace) to the operator's configured namespace. It is a no-op in the default install where the two are identical.

type ExecutionResult added in v0.3.0

type ExecutionResult struct {
	Results []OperationResult

	// Stale names the objects that were written successfully but against a
	// cluster state the plan did not anticipate, so the pass should run again
	// from an accurate read. The operations themselves succeeded and are
	// reported as such; this is a request to re-plan, not a failure.
	Stale []ObjectRef

	// Deferred names the objects that were not written this pass because the
	// cluster moved under it. Like Stale this asks for a short requeue rather
	// than reporting a failure.
	Deferred []ObjectRef
}

ExecutionResult is the outcome of executing a whole plan.

func (ExecutionResult) DeferredResults added in v0.3.0

func (r ExecutionResult) DeferredResults() []OperationResult

DeferredResults returns the operations that were not written this pass because the cluster moved under it.

func (ExecutionResult) Dropped added in v0.3.0

func (r ExecutionResult) Dropped() []OperationResult

Dropped returns the operations removed from the plan before execution.

func (ExecutionResult) Err added in v0.3.0

func (r ExecutionResult) Err() error

Err joins every failure into a single error, or returns nil when the plan executed cleanly. Skipped operations do not contribute: the dependency that caused them already reported the underlying failure, and repeating it would bury the real cause in a Site condition.

func (ExecutionResult) Failed added in v0.3.0

func (r ExecutionResult) Failed() []OperationResult

Failed returns the operations that were attempted and failed.

func (ExecutionResult) Skipped added in v0.3.0

func (r ExecutionResult) Skipped() []OperationResult

Skipped returns the operations that were never attempted because a dependency failed.

type ObjectRef added in v0.3.0

type ObjectRef struct {
	GVK       schema.GroupVersionKind
	Namespace string
	Name      string
}

ObjectRef identifies an object for dependency declarations. It is comparable, so it works as a map key.

func RefOf added in v0.3.0

RefOf returns the ObjectRef identifying obj.

func (ObjectRef) String added in v0.3.0

func (r ObjectRef) String() string

String renders an ObjectRef for error messages, in a shape that reads well in a Site condition.

type OpKind added in v0.3.0

type OpKind int

OpKind is what the executor does with an Operation's object.

Reconciliation is not reducible to "produce objects, apply them": components also create ConfigMaps only when absent so user payloads survive, adopt existing objects by patching owner references under optimistic lock, merge operator-owned keys into user-owned config content, and delete objects that should no longer exist. A plan carries the intent, and the executor is the only thing that writes.

const (
	// OpApply server-side applies the object, taking ownership of every field
	// it declares. This is the default for operator-owned objects.
	OpApply OpKind = iota

	// OpCreateIfAbsent creates Object only when nothing exists at its key, and
	// never overwrites an existing payload. Used for component ConfigMaps,
	// whose contents are a documented user escape hatch. An AlreadyExists race
	// is success: whoever won owns the payload.
	OpCreateIfAbsent

	// OpMergePatch patches from Base to Object under optimistic lock, leaving
	// every field neither mentions untouched. Base carries the observed state
	// the plan was computed from, including its resourceVersion, so a
	// concurrent edit produces a conflict and the pass retries rather than
	// silently clobbering. Used for owner-reference adoption and for merging
	// operator-owned keys into user-owned config content.
	OpMergePatch

	// OpDelete removes the object, treating absence as success.
	OpDelete
)

func (OpKind) String added in v0.3.0

func (k OpKind) String() string

String renders an OpKind for error messages and test failures.

type OpStatus added in v0.3.0

type OpStatus int

OpStatus is the outcome of a single operation.

const (
	// OpSucceeded means the write completed, or was unnecessary.
	OpSucceeded OpStatus = iota

	// OpFailed means the write was attempted and returned an error.
	OpFailed

	// OpSkipped means the operation was never attempted because a dependency
	// failed. Skipping rather than attempting is deliberate: applying a
	// workload whose ConfigMap failed to write would produce a pod that cannot
	// mount, which is worse than not writing the workload at all.
	OpSkipped

	// OpDeferred means the operation was not written this pass, was not a
	// failure, and will be attempted again shortly.
	//
	// Two things produce it, and they share a shape: the cluster moved under a
	// pass that had already read it. An optimistic-lock conflict means another
	// writer won the object, and a dependency that turned out to already exist
	// means the plan was computed from a read that is no longer true. Neither
	// is a fault of the component, neither is worth an error, and both are
	// resolved by re-planning against what is actually there.
	//
	// It is distinct from OpSkipped because a skip means something failed, and
	// reporting a lost race as a dependency failure flapped a Site condition to
	// False for the second it took to re-plan.
	OpDeferred

	// OpDropped means the operation was removed from the plan before execution,
	// because the overrides that would have shaped it could not be used. The
	// executor never sees it, so it is recorded by whatever removed it.
	//
	// It exists because "removed from the plan" and "never planned" are
	// different, and only the first should stop a component reporting itself
	// reconciled. Without it a component whose workload was withheld still
	// reported Reconciled from its planning verdict, while the Site's override
	// status said Degraded about the very same object.
	OpDropped
)

func (OpStatus) String added in v0.3.0

func (s OpStatus) String() string

String renders an OpStatus for error messages and test failures.

type Operation added in v0.3.0

type Operation struct {
	Kind   OpKind
	Object *unstructured.Unstructured

	// Base is the observed state for OpMergePatch, and is ignored otherwise.
	// The executor computes the patch from Base to Object.
	Base *unstructured.Unstructured

	// Component names the component that planned this operation, and Site the
	// Site it was planned for. Site is empty for cluster-scoped operations.
	// Both are used to attribute results back to a Site condition.
	Component string
	Site      string

	// Overridable marks the workloads user-supplied overrides may target. Only
	// these are merge candidates, and only these are dropped when an override
	// document fails preflight, so an override typo cannot stop RBAC, Services
	// or ConfigMaps from reconciling.
	Overridable bool

	// SharedKey, when non-empty, identifies an operation that is identical
	// across Sites and must execute once per pass. Per-Site planning otherwise
	// re-applies shared support objects once per Site.
	SharedKey string

	// DependsOn declares ordering. The executor runs dependencies first and
	// skips dependents when a dependency fails, so a failure does not leave a
	// workload pointing at a ConfigMap that was never written.
	DependsOn []ObjectRef
}

Operation is one unit of desired state a component wants written.

func ApplyOperations added in v0.3.0

func ApplyOperations(objects []*unstructured.Unstructured, componentName, site string) []Operation

ApplyOperations wraps decoded objects as apply operations attributed to a component and Site. Site is empty for cluster-scoped components.

func DeleteOperation added in v0.3.0

func DeleteOperation(obj client.Object, componentName, site string) Operation

DeleteOperation builds a delete operation for a typed object.

func NamespaceOperation added in v0.3.0

func NamespaceOperation(namespace string) Operation

NamespaceOperation returns the single operation that reconciles the namespace the operator installs components into.

Every component's embedded manifests carry a Namespace, because each set is also meant to be installable on its own with kubectl. Reconciling them all meant applying the same object once per component and, for per-Site components, once per Site. That was not merely redundant: the manifests do not agree on the labels, and they are applied under a single field owner, so whichever component ran last won and the label changed on every pass.

Server-side apply makes one owner the right answer rather than a convenience. A field owner that stops declaring a field it previously owned removes that field, so two owners alternating over one object is a write loop, not a race that eventually settles.

The operator's own manifests already create this namespace, since the operator has to run somewhere, so in practice this operation adopts an existing object rather than creating one. It is still planned every pass so the namespace is recreated if it is deleted out from under a running operator.

func (Operation) Ref added in v0.3.0

func (o Operation) Ref() ObjectRef

Ref returns the ObjectRef identifying this operation's object.

type OperationResult added in v0.3.0

type OperationResult struct {
	Ref       ObjectRef
	Kind      OpKind
	Component string
	Site      string
	Status    OpStatus
	Err       error
}

OperationResult is the outcome of one operation, attributed to the component and Site that planned it.

A deduplicated shared operation produces one OperationResult per contributor, all carrying the same Status, so every Site that depended on it sees the outcome even though the write happened once.

type Plan added in v0.3.0

type Plan struct {
	Operations []Operation
}

Plan is the set of operations a component wants executed for one pass.

Planning may read cluster state, because decisions like "does this ConfigMap already exist" and "is this singleton retained" depend on it. Planning must not write: that is the executor's job, and it is what makes preflight validation atomic.

func NewPlan added in v0.3.0

func NewPlan() *Plan

NewPlan returns an empty plan.

func (*Plan) Add added in v0.3.0

func (p *Plan) Add(ops ...Operation)

Add appends operations to the plan. A nil plan receiver is not valid; use NewPlan.

func (*Plan) ExecutionOrder added in v0.3.0

func (p *Plan) ExecutionOrder() (string, error)

ExecutionOrder renders the order the executor will run this plan in, one operation per line, without running any of it.

Summary renders the order components emitted their operations in; this renders what actually happens. The distinction cost real defects: execution order was changed while every golden plan test kept passing, because the executor sorts a copy and Summary reads the original. Components pin what they intend to write with Summary; this pins what the cluster sees.

func (*Plan) Len added in v0.3.0

func (p *Plan) Len() int

Len reports the number of operations in the plan, treating a nil plan as empty so callers can test emptiness without a nil check.

func (*Plan) Merge added in v0.3.0

func (p *Plan) Merge(other *Plan)

Merge appends every operation from other. A nil other is a no-op, so callers can merge the result of a planner that returned no work.

func (*Plan) Summary added in v0.3.0

func (p *Plan) Summary() string

Summary renders the plan as one line per operation, in plan order.

It exists so tests can pin exactly what a component intends to write, and so a plan can be logged or displayed without dumping whole objects. The reaper gates its migration on the objects and annotations components produce, so an object silently appearing, disappearing or being renamed is a real hazard.

type Registry

type Registry struct {
	Cluster []ClusterComponent
	Site    []SiteComponent
}

Registry is the ordered set of components the SiteReconciler drives. Cluster components run in every pass; Site components run only when a Site is present. Conditions are published Cluster-first then Site, so the order of these slices is the stable condition order on the Site status.

func (*Registry) Knows added in v0.3.0

func (r *Registry) Knows(name string) bool

Knows reports whether a component name belongs to this registry.

The executor also runs operations no component owns, such as the namespace every component installs into. Their results are attributed to a name the registry does not know, so nothing publishing per-component conditions can report them, and without this the reconciler had no way to notice.

func (*Registry) Validate

func (r *Registry) Validate() error

Validate rejects an empty registry and duplicate Name or ConditionType values across both lists, so a misconfigured registry fails fast at startup rather than silently dropping or double-publishing a condition.

type Result

type Result struct {
	Ready   bool
	Reason  string
	Message string
	Err     error

	// RequeueAfter, when positive, asks the driver to re-run the Site after the
	// given duration even without an error or a watch event. The driver uses the
	// smallest positive RequeueAfter across all components in a pass.
	RequeueAfter time.Duration
}

Result is the outcome of reconciling a single component for one pass. The SiteReconciler translates it into a Site status condition (Ready -> ConditionTrue/False, Reason, Message) and aggregates Err into the reconcile error so a failing component requeues the Site.

A not-ready Result without an Err does not by itself trigger a retry: the driver requeues on Err (with backoff) or after the smallest positive RequeueAfter across components. A component that is waiting on state it does not watch should set RequeueAfter (see NotReadyAfter) so it is re-checked; otherwise it only re-runs when one of its watches fires.

func CombineResult added in v0.3.0

func CombineResult(componentName, site string, planned Result, exec ExecutionResult) Result

CombineResult folds a component's execution outcomes for one Site into its planning verdict.

Planning reaches verdicts execution cannot, such as Disabled or NoSites, so those survive when there was nothing to execute. When operations did run, a failure replaces the planning verdict, because a component that planned successfully but failed to write is not reconciled.

Results are matched on both component and Site. A cluster component is recorded on every Site's status, and a per-Site component plans separately for each Site, so without the Site filter a failure writing one Site's DaemonSet appeared as a reconcile error on every other Site as well: every Site went NotReady and the condition message named an object belonging to a Site the reader was not looking at. Operations with no Site are cluster scoped and count towards every Site.

Skipped operations do not produce a failure, because the dependency that caused them already reported the underlying error and repeating it here would bury the real cause. They do prevent a Ready verdict: the component did not write what it planned, and reporting Reconciled would claim otherwise.

Deferred operations do neither. Nothing failed and nothing is wrong; the cluster moved under the pass and the next one, a second later, writes what this one planned. Reporting that as not-Ready would flap a Site condition on every lost race, which is noise rather than signal.

func Disabled

func Disabled(message string) Result

Disabled reports a component that is intentionally not running.

func Failed

func Failed(err error) Result

Failed reports a component reconcile error. The error is surfaced both as the condition message and as the aggregated reconcile error.

func NoSites

func NoSites(message string) Result

NoSites reports a cluster component retained while no Sites exist.

func NotReady

func NotReady(reason, message string) Result

NotReady reports a component that is not yet ready for a caller-supplied reason without treating it as a reconcile error. It does not schedule a retry on its own; use NotReadyAfter, set Err, or rely on a watch to re-trigger.

func NotReadyAfter

func NotReadyAfter(reason, message string, after time.Duration) Result

NotReadyAfter reports a not-ready component and asks the driver to re-check the Site after the given duration, for readiness that depends on state the component does not watch (for example polling an external resource).

func NotReadyErr added in v0.3.0

func NotReadyErr(reason string, err error, after time.Duration) Result

NotReadyErr reports a component that could not establish readiness because a read failed, under a caller-supplied reason rather than ReasonReconcileError.

It exists for the case where a failed read is not a reason to stop reconciling. A component that cannot answer one question about its own readiness still knows what the rest of its manifests should look like, so it emits them and withholds only what the unanswered question governs. The error is still surfaced, both in the condition message and as the aggregated reconcile error, so the pass goes through error backoff and the failure is not mistaken for an orderly wait.

func Reconciled

func Reconciled() Result

Reconciled reports a successfully reconciled component.

func ReconciledAfter added in v0.3.0

func ReconciledAfter(message string, after time.Duration) Result

ReconciledAfter reports a successfully reconciled component and asks the driver to check it again after the given duration.

It takes a message rather than reusing Reconciled's, because a component that wants to be re-checked is waiting on something, and the condition is the only place that can say what. "component reconciled" on a Site that is quietly polling tells a reader nothing about why.

type SiteComponent

type SiteComponent interface {
	// Name is the stable, unique component identifier (for example "metalman").
	Name() string

	// ConditionType is the Site status condition type published for this
	// component (for example "MetalmanReady").
	ConditionType() string

	// Enabled reports whether the component should run for the Site.
	Enabled(site *unboundedv1alpha3.Site) bool

	// Plan computes the operations that would apply the component's desired
	// state for the Site. The same read-not-write rule as ClusterComponent.Plan
	// applies.
	Plan(ctx context.Context, env *Env, site *unboundedv1alpha3.Site) (*Plan, Result, error)

	// CleanupPlan computes the operations that remove the component's per-Site
	// resources when it is disabled.
	CleanupPlan(ctx context.Context, env *Env, site *unboundedv1alpha3.Site) (*Plan, Result, error)
}

SiteComponent is a per-Site unit of desired state (for example metalman). The SiteReconciler runs it only when a Site is present, so Plan and CleanupPlan always receive a non-nil Site. The driver owns the enable/disable branch: it calls Plan when Enabled reports true and CleanupPlan when it reports false (or the Site is deleted via owner-reference garbage collection).

Plan runs on every Site event and must be idempotent. To self-heal owned resources on drift or deletion, set a controller owner reference (see Env.SiteOwnerReference) and register Owns via WatchProvider; a not-ready Result requeues only through Result.Err or Result.RequeueAfter.

type WatchProvider

type WatchProvider interface {
	SetupWatches(b *builder.Builder, env *Env)
}

WatchProvider is implemented by a ClusterComponent or SiteComponent that owns or watches resources whose churn should re-trigger its reconcile. The SiteReconciler calls SetupWatches on every component that implements it when wiring the controller.

Jump to

Keyboard shortcuts

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