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 storage) 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
- Variables
- func ConfigMapPayloadChanged(oldConfig, newConfig *corev1.ConfigMap) bool
- func ConfigMapPayloadHash(config *corev1.ConfigMap) string
- func IsRBACObject(obj *unstructured.Unstructured, nameContains string) bool
- func SetNamedContainerImage(obj *unstructured.Unstructured, name, image string) error
- func SetPodSpecImages(obj *unstructured.Unstructured, image string) error
- func SiteNodeAffinity(siteName string) *corev1.Affinity
- func SiteOwnerReference(site *unboundedv1alpha3.Site) metav1.OwnerReference
- func ToUnstructured(obj client.Object) *unstructured.Unstructured
- func UpsertOwnerReference(refs []metav1.OwnerReference, owner metav1.OwnerReference) ([]metav1.OwnerReference, bool)
- func YamlFiles(fsys fs.FS) ([]string, error)
- type ClusterComponent
- type Config
- type Env
- func (e *Env) ApplyObject(ctx context.Context, obj client.Object) error
- func (e *Env) DecodeManifestFS(manifests fs.FS, mutate func(*unstructured.Unstructured) error) ([]*unstructured.Unstructured, error)
- func (e *Env) DecodeManifestFiles(manifests fs.FS, files []string, mutate func(*unstructured.Unstructured) error) ([]*unstructured.Unstructured, error)
- func (e *Env) DefaultConfigMap(manifests fs.FS, name, component string) (*corev1.ConfigMap, error)
- func (e *Env) DeleteIfExists(ctx context.Context, obj client.Object) error
- func (e *Env) Execute(ctx context.Context, plan *Plan) (ExecutionResult, error)
- func (e *Env) InNamespaceNamed(names ...string) func(client.Object) bool
- func (e *Env) InNamespaceWithPrefix(prefix string) func(client.Object) bool
- func (e *Env) ListSites(ctx context.Context) ([]unboundedv1alpha3.Site, error)
- func (e *Env) LiveReader() client.Reader
- func (e *Env) ManagedConfigPredicate(match func(client.Object) bool) predicate.Predicate
- func (e *Env) ManagedWorkloadPredicate(match func(client.Object) bool) predicate.Predicate
- func (e *Env) OwnedWorkloadPredicate() predicate.Predicate
- func (e *Env) RequestSingleton() handler.EventHandler
- func (e *Env) RequestSiteFromConfigName(prefix string) handler.EventHandler
- func (e *Env) RetargetNamespace(obj *unstructured.Unstructured)
- type ExecutionResult
- type ObjectRef
- type OpKind
- type OpStatus
- type Operation
- type OperationResult
- type Plan
- type Registry
- type Result
- func CombineResult(componentName, site string, planned Result, exec ExecutionResult) Result
- func Disabled(message string) Result
- func Failed(err error) Result
- func NoSites(message string) Result
- func NotReady(reason, message string) Result
- func NotReadyAfter(reason, message string, after time.Duration) Result
- func NotReadyErr(reason string, err error, after time.Duration) Result
- func Reconciled() Result
- func ReconciledAfter(message string, after time.Duration) Result
- type SiteComponent
- type WatchProvider
Constants ¶
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" // 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 )
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.
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 ¶
var DefaultNamespace = unbounded.SystemNamespace()
DefaultNamespace is the namespace the operator installs components into. It follows the operator's own namespace (unbounded.SystemNamespace()).
Functions ¶
func ConfigMapPayloadChanged ¶
ConfigMapPayloadChanged reports whether two ConfigMaps differ in payload.
func ConfigMapPayloadHash ¶
ConfigMapPayloadHash hashes the complete ConfigMap payload. JSON provides a deterministic encoding for string-keyed maps, including binary values.
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 ¶
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.
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.
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 ¶
ApplyObject server-side applies a single object with the operator field owner.
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 ¶
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 ¶
DeleteIfExists deletes an object, treating an already-absent object as success.
func (*Env) Execute ¶ added in v0.3.0
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 ¶
InNamespaceNamed returns a matcher for objects in the operator namespace whose name is one of names.
func (*Env) InNamespaceWithPrefix ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
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
func RefOf(obj *unstructured.Unstructured) ObjectRef
RefOf returns the ObjectRef identifying obj.
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 )
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 )
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
// 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
DeleteOperation builds a delete operation for a typed object.
func NamespaceOperation ¶ added in v0.3.0
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.
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 (*Plan) Add ¶ added in v0.3.0
Add appends operations to the plan. A nil plan receiver is not valid; use NewPlan.
func (*Plan) ExecutionOrder ¶ added in v0.3.0
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
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
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
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
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.
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 Failed ¶
Failed reports a component reconcile error. The error is surfaced both as the condition message and as the aggregated reconcile error.
func NotReady ¶
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 ¶
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
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 ReconciledAfter ¶ added in v0.3.0
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 or storage). 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 ¶
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.