Documentation
¶
Overview ¶
Package resourceapplier reconciles template-declared Kubernetes resources via Server-Side Apply (SSA).
Mirrors statusapplier's leader-only / checksum-cached / event-driven shape exactly — it just operates on full resources rather than status sub-paths. Templates declare desired resources under spec.k8sResources; the renderer renders each and surfaces them on ReconciliationCompletedEvent.RenderedResources; this component applies them on the cluster after the render+validate pipeline succeeds.
The applier is stateless on the success path. RenderedResources travel with the ReconciliationCompletedEvent that triggers the apply — there is no side-channel cache. Patches/resources on a completion event are tautologically the ones for the configuration that completion describes, so no LATEST-vs-completed race is possible (the same contract that statusapplier's CLAUDE.md spells out for StatusPatches).
Resource-agnostic by design: the controller never names a specific resource kind — it just applies whatever the template emits. Templates decide what to emit; the controller is the generic vehicle.
API-traffic safety:
- SHA-256 checksum cache per (namespace, name, gvr) skips the SSA round- trip when the payload matches the last-applied value.
- Cache is cleared on BecameLeaderEvent (the previous leader's checksums aren't trustworthy for the new one), forcing a single re-apply burst on leadership transitions but no hammering on steady-state renders.
- Default RestrictToOwnNamespace=true refuses cross-namespace and cluster-scoped applies; opt-in via config for templates that need to spawn cluster-scoped resources (corresponding ClusterRole RBAC must also be granted).
Orphan pruning: resources that disappear from the rendered set between reconciliations are detected via the in-memory checksum cache (key in cache but not in new render) and deleted. Startup orphans (resources the previous incarnation created and never cleaned up before crashing) require an offline kubectl sweep using the managed-by label this component injects.
Index ¶
Constants ¶
const ( // ComponentName is the unique identifier for this component. ComponentName = "resource-applier" // EventBufferSize is the size of the event subscription buffer. // High volume: reconciliation.completed fires on every reconcile. Use a // Publishing-tier buffer so churn bursts don't drop these (coalescible) // events before this applier drains them. EventBufferSize = busevents.PublishingSubscriberBuffer // LabelManagedBy is injected onto every applied resource so operators // can locate everything the controller owns with a single // `kubectl get … -l haproxy-haptic.org/managed-by=<name>` selector. LabelManagedBy = "haproxy-haptic.org/managed-by" // DefaultManagedByValue is the value injected into LabelManagedBy when // the chart doesn't override Config.ManagedByValue. Distinct deployments // in the same namespace should set their own values. DefaultManagedByValue = "haptic-controller" // AnnotationOwnership lets templates flag a rendered resource as // jointly owned with another field manager (helm / argocd / kubectl). // When set to OwnershipPartial, the applier: // - does NOT inject the managed-by label (the resource isn't // ours to claim end-to-end); // - does NOT track the resource for orphan-delete (vanishing from // the rendered set must release SSA-owned fields, never delete // the whole object — that would clobber the chart's static // spec); // - always strips the annotation from the payload before SSA so // it remains a controller-internal flag. // SSA's per-list-map-entry ownership (e.g. Service.spec.ports keyed // by (port, protocol)) handles the actual field-level merge with // the other field manager. AnnotationOwnership = "haproxy-haptic.org/ownership" // OwnershipPartial is the AnnotationOwnership value that activates // partial-ownership mode. Any other value (including absence) means // full ownership: existing behaviour, unchanged. OwnershipPartial = "partial" )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Component ¶
Component reconciles template-declared resources to the cluster.
All-replica subscriber, leader-only applier — same shape as statusapplier.Component. State (cachedResources, checksum cache) lives only on the active leader; replicas in standby just observe events.
func New ¶
New constructs an applier and subscribes to the events it needs. Subscription happens in the constructor so events buffered before EventBus.Start() are delivered after — the same all-replica pattern every haptic controller component follows.
func (*Component) CoalescesOn ¶
CoalescesOn opts this applier into component.Base's mailbox coalescing: under churn only the LATEST reconciliation.completed matters (it carries the latest rendered resources, superseding earlier ones), so runs of them collapse in the mailbox and the bus can never overflow this subscriber.
func (*Component) HandleEvent ¶
HandleEvent implements component.EventHandler: it fans out by event type, tracking processing time for the health check. Mirror of statusapplier's HandleEvent shape — different events because resource lifecycle is "rendered → deployed (= apply)" rather than the four-phase status patches use.
func (*Component) HealthCheck ¶
HealthCheck returns nil if the component is healthy.
type Config ¶
type Config struct {
EventBus *busevents.EventBus
DynamicClient dynamic.Interface
// DiscoveryClient is used on leader-acquire to enumerate every
// namespace-scoped API resource type the cluster supports, so the
// applier can rebuild its in-memory `lastAppliedKeys` from cluster
// state via the managed-by label selector. Without this, resources
// the controller applied before a crash but whose desired state was
// removed while the controller was down (e.g. the user deleted the
// parent resource during a controller upgrade) would leak as orphans
// until manually swept. Optional: when nil, startup-orphan recovery is
// skipped and operators must rely on the
// `kubectl get … -l haproxy-haptic.org/managed-by=<name>` mitigation.
DiscoveryClient discovery.DiscoveryInterface
GVRResolver GVRResolver
Logger *slog.Logger
// OwnNamespace is the namespace the controller pod runs in. Required
// when RestrictToOwnNamespace is true.
OwnNamespace string
// RestrictToOwnNamespace, when true (default for the chart), refuses
// to apply any rendered resource whose namespace is empty (cluster-
// scoped) or differs from OwnNamespace. Combined with the chart's
// namespace-scoped Role, this gives belt-and-suspenders safety: even
// a misbehaving template can't escalate beyond the controller's
// namespace.
RestrictToOwnNamespace bool
// ManagedByValue is the label value injected as
// `haproxy-haptic.org/managed-by`. Defaults to the controller name
// ("haptic-controller") so multiple haptic deployments in the same
// cluster don't clobber each other's managed sets.
ManagedByValue string
// OwnerRef identifies the HAProxyTemplateConfig CR that owns the
// applied resources. The applier injects an `ownerReferences`
// entry pointing at this object on every full-ownership SSA
// payload, with `controller: true` and `blockOwnerDeletion: true`
// so Kubernetes garbage collection cascade-deletes the rendered
// resources when the CR is removed (e.g. `helm uninstall`).
//
// Optional: when zero (UID empty), no OwnerReference is injected.
// Partial-ownership entries never get an OwnerReference regardless
// (the chart-static or other field manager already owns the
// resource end-to-end).
OwnerRef OwnerReference
}
Config bundles the dependencies a New caller must provide.
type GVRResolver ¶
type GVRResolver = statusapplier.GVRResolver
GVRResolver resolves apiVersion + kind to a GroupVersionResource. Reused from the statusapplier package to avoid duplicate logic.