Help us shape the future of Go! Take the 2026 Go Developer Survey

statusapplier

package
v0.9.0 Latest Latest
Warning

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

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

README

pkg/controller/statusapplier

Applies template-driven status patches to Kubernetes resources via Server-Side Apply (SSA).

Overview

Templates can register status patches against arbitrary Kubernetes resources (typically the Ingress / HTTPRoute / Gateway whose configuration was just rendered) using the pkg/templating.StatusPatch API. Each registered patch carries variants keyed by pipeline outcome — rendered (rendering succeeded), deployed (deployment succeeded), renderFailed, deployFailed. This component subscribes to the lifecycle events, picks the right variant, and applies it via SSA with a phase-scoped field manager (haptic-rendered, haptic-deployed, haptic-renderFailed, haptic-validateFailed, or haptic-deployFailed) so each phase owns disjoint condition entries and composes cleanly with patches from other controllers.

It runs on every replica (subscribes in the constructor like other all-replica components) but only the leader actually issues SSA patches. The component is stateless. Successful lifecycle events carry one sealed render occurrence, and the applier reads the authenticated status snapshot from it. Public event fields are diagnostic shadows and never authorize an apply.

Quick Start

import (
    "k8s.io/client-go/dynamic"

    "github.com/haproxy-haptic/HAPTIC/pkg/controller/statusapplier"
)

applier := statusapplier.New(&statusapplier.Config{
    EventBus:      bus,
    DynamicClient: dynamicClient,
    GVRResolver:   statusapplier.NewRestMapperResolver(restMapper),
    Logger:        logger,
})
go applier.Start(ctx)

GVRResolver is an interface so tests can supply a fake. NewRestMapperResolver(mapper) uses the controller's meta.RESTMapper to resolve apiVersion + kind from cluster discovery, including each CRD's spec.names.plural. Custom resources with irregular plurals need no special resolver.

Why templates register variants

Templates supply the resource-specific condition fields and every outcome variant during rendering. Later pipeline events select a variant without rerendering the templates or adding resource-specific logic to Go. SSA respects the target schema's list-map merge keys, so another controller's condition entries can coexist with HAPTIC's entries.

After a render failure, the coordinator supplies the status snapshot from its last successful render. A resource that has never appeared in a successful render has no registered failure variant; inspect the controller's render error rather than expecting a new status condition on that resource.

Event Flow

Event Action
ResourcesAppliedEvent Read the authenticated occurrence and apply its rendered variant if leader
DeploymentCompletedEvent Read the authenticated occurrence and apply deployed when every pod runs it or deployFailed when a pod failed
DeploymentSkippedEvent Read the authenticated occurrence and apply deployed if leader
ReconciliationFailedEvent Apply renderFailed or deployFailed variant (depending on which phase failed) if leader
BecameLeaderEvent Flip the leader flag on; clear the status apply cache so the next reconciliation writes the new leader's status
LostLeadershipEvent Flip the leader flag off; in-flight handlers re-check via leaderRLocked()

SSA Conflict Handling

Each apply uses a phase-scoped field manager (e.g. haptic-rendered, haptic-deployed). The SSA calls use Force: true (metav1.PatchOptions{Force: new(true)}), so conflicting field ownership is taken from any other manager rather than returning a 409 Conflict. This means haptic always wins field-ownership races; there is no conflict-retry path.

See Also

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package statusapplier applies template-driven status patches to Kubernetes resources.

The StatusApplier is a stateless consumer: successful lifecycle events carry a sealed render occurrence, and the applier reads its authenticated status snapshot. There is no side-channel cache or mutable-shadow fallback.

Status patches are fully defined by templates — the controller never hardcodes knowledge of specific resource types or condition names. Templates register patches via the statusPatch() template function during rendering, including outcome-keyed variants for each pipeline phase (rendered, deployed, renderFailed, deployFailed).

Event mapping:

  • ResourcesAppliedEvent: apply the "rendered" variant from the sealed occurrence after the same render's resources were applied. Ordering matters: conditions like Accepted=True must not precede the infrastructure resources they describe, so the rendered variant rides the post-apply event rather than TemplateRenderedEvent.
  • DeploymentCompletedEvent: apply the "deployed" variant from the sealed occurrence that deployment carried. Programmed=True genuinely means "HAProxy is serving this config" because reload verification gates DeploymentCompletedEvent.
  • DeploymentSkippedEvent: apply the "deployed" variant from the sealed occurrence. Same data-plane-is-converged semantics as DeploymentCompletedEvent, reached by the scheduler determining the data plane is already at this config. Without this branch, any resource whose addition or update produces no config change (a status-only delta) would stay at the CRD-default condition state indefinitely.
  • ReconciliationFailedEvent: apply the failure variant ("renderFailed" / "deployFailed") from event.StatusPatches. The Coordinator forwards the patches from the last successful render — failure paths don't produce fresh patches, so a "last good" snapshot is the only thing the chart's failure variants can be applied against.

Leader transitions: the Reconciler triggers an immediate reconciliation on BecameLeaderEvent (per pkg/controller/reconciler/CLAUDE.md), producing a fresh render whose patches arrive via ResourcesAppliedEvent. The applier therefore has no replay responsibility on leadership change.

Index

Constants

View Source
const (
	// ComponentName is the unique identifier for this component.
	ComponentName = "status-applier"

	// EventBufferSize is the size of the event subscription buffer.
	// Moderate volume: receives template rendered, reconciliation completed/failed,
	// and leadership events.
	// High volume: template.rendered fires on every reconcile. Even with the
	// coordinator coalescing renders, an occasional slow SSA apply can briefly
	// back this up, so use a Publishing-tier buffer to avoid dropping the
	// (coalescible) template.rendered / deployment.completed events — a dropped
	// deployment.completed leaves Programmed=True unapplied until the next deploy.
	EventBufferSize = busevents.PublishingSubscriberBuffer
)

Variables

This section is empty.

Functions

func IsRetriable

func IsRetriable(err error) bool

IsRetriable returns true if the error is likely transient and the operation should be retried on the next reconciliation cycle. It is exported so the resourceapplier shares this single retry policy rather than duplicating it.

Types

type Component

type Component struct {
	*component.Base
	// contains filtered or unexported fields
}

Component applies template-driven status patches to Kubernetes resources via Server-Side Apply (SSA).

This is an all-replica component that subscribes in the constructor and applies the appropriate variant based on pipeline lifecycle events. Only the leader applies patches to avoid conflicts.

Event flow (success phases read the authenticated occurrence snapshot):

ResourcesAppliedEvent → apply "rendered" variant (if leader)
DeploymentCompletedEvent → apply "deployed" variant (if leader)
DeploymentSkippedEvent → apply "deployed" variant (if leader); the data
    plane is already at the rendered config so Programmed conditions
    should reflect the current generation
ReconciliationFailedEvent → apply "renderFailed" or "deployFailed" variant (if leader)
BecameLeaderEvent → clear status cache; rely on Reconciler to fire a fresh reconcile
LostLeadershipEvent → flip the leader flag off

func New

func New(cfg *Config) *Component

New creates a new StatusApplier component.

The component subscribes to events in the constructor (all-replica pattern). It only applies patches when it is the leader.

func (*Component) CoalescesAcrossQueue

func (c *Component) CoalescesAcrossQueue() bool

CoalescesAcrossQueue makes the latest-wins statement hold across the whole mailbox, not just within a run: under churn the three types arrive strictly alternating (render → resources.applied, deploy → deployment.completed), so run-only collapsing never fired and the queue grew to 2048 full patch sets (6 GB live heap on a 1,900-route churn). Each event carries the complete patch set for its phase, so only the newest of each type matters.

func (*Component) CoalescesOn

func (c *Component) CoalescesOn() []string

CoalescesOn opts this applier into component.Base's mailbox coalescing. All three declared types are latest-wins FOR THIS COMPONENT: rendered patches ride every ResourcesAppliedEvent, and the deployed variant rides every DeploymentCompleted/SkippedEvent — each event carries the FULL current patch set, so only the newest of an uninterrupted run matters. Collapsing runs keeps the mailbox queue bounded by the deploy cadence instead of the render rate: without it a burst of deployment events (each costing an SSA fan-out to apply) backlogs the queue and status latency grows unboundedly (observed: 512-deep backlog and 90s Programmed lag in gateway-api conformance).

func (*Component) HandleEvent

func (c *Component) HandleEvent(event busevents.Event)

HandleEvent implements component.EventHandler: it routes events to the appropriate handler, tracking processing time for the health check.

func (*Component) HealthCheck

func (c *Component) HealthCheck() error

HealthCheck returns nil if the component is healthy.

func (*Component) Start

func (c *Component) Start(ctx context.Context) error

Start captures the loop context for handlers and runs the embedded component.Base event loop until the context is cancelled.

type Config

type Config struct {
	// EventBus is the event bus for subscribing to events and publishing results.
	EventBus *busevents.EventBus

	// DynamicClient is the Kubernetes dynamic client for SSA patch operations.
	DynamicClient dynamic.Interface

	// GVRResolver resolves apiVersion + kind to GroupVersionResource.
	GVRResolver GVRResolver

	// Logger is the structured logger.
	Logger *slog.Logger

	// SelfWrites records the resourceVersion of every applied patch so the
	// watchers can refresh from the echoed event without re-rendering.
	// Optional.
	SelfWrites *k8stypes.SelfWriteRegistry
}

Config contains configuration for creating a StatusApplier Component.

type GVRResolver

type GVRResolver interface {
	Resolve(apiVersion, kind string) (schema.GroupVersionResource, error)
}

GVRResolver resolves apiVersion + kind to a GroupVersionResource. This abstracts the REST mapper for testability.

type RestMapperResolver

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

RestMapperResolver implements GVRResolver by consulting a Kubernetes RESTMapper. The kind→resource mapping comes from the cluster's discovery data, so any watched resource — including a CRD with an irregular or fully custom plural — resolves correctly, with no hardcoded or guessed pluralization (RULE #1: the controller stays resource-agnostic).

func NewRestMapperResolver

func NewRestMapperResolver(mapper meta.RESTMapper) *RestMapperResolver

NewRestMapperResolver creates a GVRResolver backed by the given RESTMapper.

func (*RestMapperResolver) Resolve

func (r *RestMapperResolver) Resolve(apiVersion, kind string) (schema.GroupVersionResource, error)

Resolve maps apiVersion + kind to a GroupVersionResource by consulting the RESTMapper. The resource name comes from the cluster's discovery data (and, for CRDs, each CRD's own spec.names.plural), so irregular and fully custom plurals resolve correctly — there are no resource-specific pluralization rules in Go (RULE #1). An unknown kind returns an error rather than a guessed plural.

Jump to

Keyboard shortcuts

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