statusapplier

package
v0.2.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 22 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 — patches travel on the events that trigger each apply, so a new leader simply relies on the Reconciler to fire a fresh reconciliation.

Quick Start

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

    "gitlab.com/haproxy-haptic/haptic/pkg/controller/statusapplier"
)

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

GVRResolver is an interface so tests can supply a fake. NewRestMapperResolver() (the default) takes no arguments and resolves apiVersion + kindGroupVersionResource via static lowercase-pluralisation, which covers the well-known Kubernetes and Gateway-API kinds (Ingress → ingresses, HTTPRoute → httproutes, etc.). Custom resources with non-standard pluralisation need a custom GVRResolver implementation.

Event Flow

Event Action
ResourcesAppliedEvent Apply the rendered variant directly from the event payload if leader (published by the ResourceApplier after the same render's resources exist — no caching, stateless)
DeploymentCompletedEvent Apply the deployed variant if leader
DeploymentSkippedEvent Apply the deployed variant if leader (deployment skipped because config unchanged)
ReconciliationFailedEvent Apply renderFailed or deployFailed variant (depending on which phase failed) if leader
BecameLeaderEvent Flip the leader flag on; clear the SSA checksum cache so the new leader writes at least once for every active resource on the next reconciliation (triggered by the Reconciler)
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: each event carries the patches it needs to apply. There is no side-channel cache. The patches travelling on a deploy event are tautologically the patches for the configuration that deploy carried — no LATEST-vs-deployed race is possible.

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 directly from event.StatusPatches (forwarded by the ResourceApplier 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 event.StatusPatches. The Deployer forwards the patches from the DeploymentScheduledEvent that triggered the deploy, so the patches describe exactly the config the deploy shipped. Programmed=True genuinely means "HAProxy is serving this config" because reload verification gates DeploymentCompletedEvent.
  • DeploymentSkippedEvent: apply the "deployed" variant from event.StatusPatches. 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 (every applied phase reads patches directly from event.StatusPatches):

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 checksum 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) 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
}

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