dataplane

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: 26 Imported by: 0

README

pkg/dataplane

Pure library for synchronising HAProxy configurations over the Dataplane API. Given a target endpoint and a desired config string (plus optional auxiliary files), the library brings HAProxy into that state by pushing the full rendered config, applying runtime-eligible server changes via the runtime API to avoid reloads whenever possible.

Module path: gitlab.com/haproxy-haptic/haptic. Source is authoritative (go doc ./pkg/dataplane); this README is a short map.

What the Library Does

  1. Parse the desired config with haproxytech/client-native (syntax).
  2. Optionally run haproxy -c on it (semantics) — see validator.go.
  3. Fetch the current config from the Dataplane API and compare section-by-section to classify the changes as runtime-eligible (server field updates) or structural.
  4. Push the full rendered config in one request: a skip_reload push carrying X-Runtime-Actions when every change is a runtime-eligible server-field update (no reload), otherwise a force_reload push.
  5. Sync auxiliary files (maps, SSL certs, general files, crt-lists) in three phases — pre-config, config, post-config — so the main config never references a file that doesn't exist yet.
  6. Retry transient connection errors and surface structured errors (SyncError, ValidationError, ParseError, ConnectionError).

Top-level API

import (
    "context"
    "log"

    "gitlab.com/haproxy-haptic/haptic/pkg/dataplane"
)

endpoint := &dataplane.Endpoint{
    URL:      "http://haproxy:5555/v3",
    Username: "admin",
    Password: "secret",
}

client, err := dataplane.NewClient(ctx, endpoint)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

result, err := client.Sync(ctx, desiredConfig, auxFiles, opts)

Endpoint is always passed as a pointer. auxFiles and opts are both nil-safe.

SyncOptions

The DefaultSyncOptions() constructor returns the recommended baseline. Behaviour fields:

opts := &dataplane.SyncOptions{
    Timeout:                   2 * time.Minute,    // overall sync deadline (default 2m)
    VerifyReload:              true,               // poll reload-status until done (default true)
    ReloadVerificationTimeout: 10 * time.Second,   // upper bound on the reload poll (default 10s)
}

SyncOptions also exposes optimisation fields the controller's pipeline populates between calls — leave them zero unless you're feeding a parser/checksum result you already have:

Field What it does
PreParsedConfig *parser.StructuredConfig Skips parsing desiredConfig if non-nil. Set by callers that already parsed the config (e.g. the validation pipeline).
CachedCurrentConfig *parser.StructuredConfig + CachedConfigVersion int64 Used together: GetVersion() is consulted first, and the expensive GetRawConfiguration()+parse round-trip is skipped if the live version on the pod matches CachedConfigVersion.
ContentChecksum string + LastDeployedChecksum string Used together: when both are set and equal, the orchestrator skips the auxiliary-file comparison entirely (no downloads from HAProxy). Drift-prevention syncs should leave LastDeployedChecksum empty to force a real check.
AuxiliaryFiles
aux := &dataplane.AuxiliaryFiles{
    GeneralFiles:    []auxiliaryfiles.GeneralFile{...},
    SSLCertificates: []auxiliaryfiles.SSLCertificate{...},
    SSLCaFiles:      []auxiliaryfiles.SSLCaFile{...},
    MapFiles:        []auxiliaryfiles.MapFile{...},
    CRTListFiles:    []auxiliaryfiles.CRTListFile{...},  // v3.2+
}

CRTListFiles is only supported on Dataplane API v3.2+; unsupported entries fail fast with a capability error rather than silently.

Sub-Package Map

Purpose Package
Public types and entry points (Client, Endpoint, SyncOptions, AuxiliaryFiles) pkg/dataplane (top level)
Three-phase sync workflow (orchestrator + comparison + raw-push apply [runtime / reload] + per-version cache + runtime API) orchestrator_*.go
HAProxy three-phase validation (haproxy -c + OpenAPI schema + client-native syntax) validate_haproxy.go, validate_schema.go, validate_syntax.go, validator.go
Version detection (local binary + remote API) and capability matrix for DP API v3.0 / v3.1 / v3.2 / v3.3 version.go, capabilities.go
Dataplane API client (dispatcher pattern, retries) client/
Config parsing via client-native parser/ (+ parser/enterprise/ for Enterprise sections)
Fine-grained diff engine comparator/ + comparator/sections/ (operations are pure descriptors; execution is raw push in orchestrator_*.go)
Raw-push execution (structural + runtime paths) orchestrator_*.go (integrated)
Auxiliary file sync (maps, SSL, SSL-CA, general files, crt-list) auxiliaryfiles/
Generated per-model OpenAPI validators validators/

Endpoint discovery (probing HAProxy pods, picking up credentials, etc.) is the controller's job in pkg/controller/discovery, not this package's.

Versioning and Capabilities

The client detects the Dataplane API version by calling /v3/info and exposes a Capabilities struct that downstream code can query without needing a live connection:

caps := client.Clientset().Capabilities()
if caps.SupportsCrtList {
    // v3.2+ only
}

For local-validation paths (where no Dataplane API is reachable), derive capabilities from a detected HAProxy binary version via dataplane.CapabilitiesFromVersion(version). Passing nil returns a conservative all-false capability set — the safe default.

All public client methods route through a single Dispatch() dispatcher so adding a new API version only touches client/dispatcher.go. pkg/dataplane/CLAUDE.md has the full walkthrough (when to use DispatchWithCapability, DispatchGeneric[T], how to add a new method).

Error Types

var syncErr *dataplane.SyncError
if errors.As(err, &syncErr) {
    // top-level sync failure with a hint and phase (stage) context
}

var valErr *dataplane.ValidationError
var parseErr *dataplane.ParseError
var connErr *dataplane.ConnectionError

For user-facing surfaces (webhook responses, CLI output), call dataplane.SimplifyValidationError(err) / dataplane.SimplifyRenderingError(err) to turn verbose library errors into a single readable line. Internal logs and metrics should keep the full chain.

Common Pitfalls

  • Skipping aux-file pre-sync. If haproxy.cfg references maps/host.map and the file hasn't been uploaded yet, HAProxy validation fails. AuxiliaryFiles plus the orchestrator handle this automatically; bypassing them is almost always a bug.
  • Hand-rolling a retry loop. The orchestrator re-resolves the config version on each sync and retries transient connection errors via client.WithRetry (3 attempts); don't layer your own retry loop on top.
  • Pushing aux-file deletes before config. Phase 3 must run after the main config is applied so we're not deleting files the live config still references.
  • Using List() patterns at the dataplane layer. This package operates on parsed config structures, not on Kubernetes stores — the .List() / .Fetch() semantics from pkg/k8s are irrelevant here.

pkg/dataplane/CLAUDE.md has the longer catalogue (retry logic, parser error wrapping, per-section comparator examples) plus the multi-version dispatch pattern and the three-phase sync rationale.

Zero-Reload Rules of Thumb

A small set of server-level changes can apply through the runtime API without reloading HAProxy:

  • Weight, Address, Port, Maintenance (enable/disable/drain)
  • AgentCheck, AgentAddr, AgentSend, HealthCheckPort
  • Frontend Maxconn
  • Map file content, ACL file content, SSL certificate content (via storage API)

Everything else — creating or deleting a server, changing check / inter / ssl settings, touching bind addresses, frontend/backend structure, rules, filters — triggers a reload. The comparator detects this and the orchestrator picks the cheapest path. To maximise zero-reload updates, templates should keep individual server lines to address:port [enabled|disabled] and push all other options into default-server.

Testing

go test ./pkg/dataplane/...          # unit + comparator tests
go test ./pkg/dataplane/... -race    # race detector

Integration tests that need a real HAProxy instance live under tests/integration and tests/acceptance.

See Also

  • pkg/dataplane/CLAUDE.md — multi-version dispatch, comparator patterns, parser quirks, testing strategies
  • pkg/dataplane/comparator/sections/ — section factories + the JSON-marshal trick for converting unified dataplaneapi.* models into per-version v3{0,1,2,3}.* types
  • pkg/controller/deployer — event adapter that wires Client.Sync into the controller's reconciliation pipeline
  • pkg/controller/discovery — probes HAProxy pods + builds the *Endpoint slice that gets handed to Sync
  • docs/controller/docs/supported-configuration.md — user-facing view of which HAProxy sections / fields are synced

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package dataplane provides a simple, high-level API for synchronizing HAProxy configurations via the Dataplane API.

The library handles all complexity internally:

  • Fetches current configuration from the Dataplane API
  • Parses both current and desired configurations
  • Computes a fine-grained ConfigDiff to classify changes as runtime-eligible server-field updates (weight, address, port, maintenance, agent checks) vs. structural changes
  • Applies the desired configuration by pushing it in full in a single request (no per-operation transactions): a skip-reload raw push carrying an X-Runtime-Actions header when every change is runtime-eligible, otherwise a force-reload raw push
  • Retries transient connection failures (the master socket is briefly down while HAProxy re-execs on reload)
  • Returns detailed results including applied changes and reload information

For production use, create a Client to reuse connections across multiple operations:

endpoint := &dataplane.Endpoint{
    URL:      "http://haproxy:5555/v3",
    Username: "admin",
    Password: "secret",
}

// Create client once, reuse for multiple operations
client, err := dataplane.NewClient(context.Background(), endpoint)
if err != nil {
    slog.Error("Failed to create client", "error", err)
    os.Exit(1)
}
defer client.Close()

desiredConfig := `
global
    daemon
defaults
    mode http
    timeout client 30s
    timeout server 30s
    timeout connect 5s
backend web
    balance roundrobin
    server srv1 192.168.1.10:80 check
`

result, err := client.Sync(ctx, desiredConfig, nil, nil)
if err != nil {
    slog.Error("Sync failed", "error", err)
    os.Exit(1)
}

fmt.Printf("Applied %d operations\n", len(result.AppliedOperations))
if result.ReloadTriggered {
    fmt.Printf("HAProxy reloaded (ID: %s)\n", result.ReloadID)
}

Simple One-Off Operations

For quick scripts, use the convenience functions (creates client internally):

result, err := dataplane.Sync(ctx, endpoint, desiredConfig, nil, nil)

Custom Options

Configure sync behavior with options:

client, err := dataplane.NewClient(ctx, endpoint)
if err != nil {
    return err
}
defer client.Close()

opts := &dataplane.SyncOptions{
    Timeout:                   3 * time.Minute, // Overall timeout
    VerifyReload:              true,            // Poll reload status after sync
    ReloadVerificationTimeout: 10 * time.Second,
}

result, err := client.Sync(ctx, desiredConfig, nil, opts)

Error Handling

The library provides detailed, actionable error messages:

client, err := dataplane.NewClient(ctx, endpoint)
if err != nil {
    return err
}
defer client.Close()

result, err := client.Sync(ctx, desiredConfig, nil, nil)
if err != nil {
    if syncErr, ok := errors.AsType[*dataplane.SyncError](err); ok {
        fmt.Printf("Stage: %s\n", syncErr.Stage)
        fmt.Printf("Error: %s\n", syncErr.Message)
        for _, hint := range syncErr.Hints {
            fmt.Printf("  Hint: %s\n", hint)
        }
    }
}

Index

Constants

This section is empty.

Variables

View Source
var ErrValidationCacheHit = errors.New("validation cache hit")

ErrValidationCacheHit is returned when validation is skipped because the same configuration was already validated successfully. Callers should use the parser cache to obtain the parsed configuration if needed.

Functions

func ComputeContentChecksum

func ComputeContentChecksum(haproxyConfig string, auxFiles *AuxiliaryFiles) string

ComputeContentChecksum generates a SHA256 checksum covering the main HAProxy config and all auxiliary files (general files, map files, SSL certificates, CRT-list files).

The checksum is used for content deduplication to skip redundant processing when config content hasn't changed. Auxiliary file slices must be pre-sorted (by AuxiliaryFiles.Sort()) to ensure deterministic results regardless of insertion order.

Returns a hex-encoded 8-byte (16 character) checksum for brevity.

func SetHAProxyExecutor

func SetHAProxyExecutor(e HAProxyExecutor) (restore func())

SetHAProxyExecutor replaces the executor used by DetectLocalVersion and semantic validation, returning a function that restores the previous one. It exists so unit tests can substitute a fake instead of shelling out; production code must not call it. Prefer installing the fake once per test package (TestMain) — per-test installs interleave badly with t.Parallel.

func SimplifyRenderingError

func SimplifyRenderingError(err error) string

SimplifyRenderingError extracts meaningful error messages from template rendering failures.

Handles template-level validation errors from the fail() function which are buried in the template engine's execution stack trace.

Input format:

"failed to render haproxy.cfg: failed to render template 'haproxy.cfg': unable to execute template: ... invalid call to function 'fail': <message>"

Output: "<message>" (the user-provided error message from fail() call)

If the error doesn't match this pattern (e.g., syntax errors, missing variables), returns the original error string.

func SimplifyValidationError

func SimplifyValidationError(err error) string

SimplifyValidationError parses HAProxy validation errors and extracts the key information for user-friendly error messages.

Handles two types of validation errors:

  1. Schema validation errors - OpenAPI spec violations: Input: "schema validation failed: configuration violates API schema constraints: ... Error at "/field": constraint" Output: "field constraint (got value)"

  2. Semantic validation errors - HAProxy binary validation failures: Input: "semantic validation failed: configuration has semantic errors: haproxy validation failed: <context>" Output: "<context>" (preserves parseHAProxyError output with context lines)

Returns original error string if parsing fails.

func ValidateConfiguration

func ValidateConfiguration(mainConfig string, auxFiles *AuxiliaryFiles, paths *ValidationPaths, version *Version, skipDNSValidation bool) (*parser.StructuredConfig, error)

ValidateConfiguration performs three-phase HAProxy configuration validation.

Phase 1: Syntax validation using client-native parser Phase 1.5: API schema validation using OpenAPI spec (patterns, formats, required fields) Phase 2: Semantic validation using haproxy binary (-c flag)

The validation writes files to the directories specified in paths. Callers must ensure that paths are isolated (e.g., per-worker temp directories) to allow parallel execution.

Validation result caching: If the same config (main + aux files + version) has been successfully validated before, the cached result is returned immediately. This is safe because HAProxy config validation is deterministic - the same inputs always produce the same result.

Parameters:

  • mainConfig: The rendered HAProxy configuration (haproxy.cfg content)
  • auxFiles: All auxiliary files (maps, certificates, general files)
  • paths: Filesystem paths for validation (must be isolated for parallel execution)
  • version: HAProxy/DataPlane API version for schema selection (nil uses default v3.0)
  • skipDNSValidation: If true, adds -dr flag to skip DNS resolution failures. Use true for runtime validation (permissive, prevents blocking when DNS fails) and false for webhook validation (strict, catches DNS issues before resource admission).

Returns:

  • *parser.StructuredConfig: The pre-parsed configuration from syntax validation (nil on cache hit or error)
  • error: nil if validation succeeds, ValidationError with phase information if validation fails

func ValidateSemantics

func ValidateSemantics(mainConfig string, auxFiles *AuxiliaryFiles, paths *ValidationPaths, skipDNSValidation bool) error

ValidateSemantics performs semantic validation using the haproxy binary (-c flag).

This function runs only Phase 2 (semantic validation) and assumes syntax/schema validation has already been done. Use this after ValidateSyntaxAndSchema() when you need to validate a modified config (e.g., with temp paths) separately from parsing.

Parameters:

  • mainConfig: The HAProxy configuration content (may have modified paths for temp directory)
  • auxFiles: All auxiliary files (maps, certificates, general files)
  • paths: Filesystem paths for validation (must be isolated for parallel execution)
  • skipDNSValidation: If true, adds -dr flag to skip DNS resolution failures

Returns:

  • error: ValidationError with phase "semantic" if validation fails

func ValidateSyntaxAndSchema

func ValidateSyntaxAndSchema(config string, version *Version) (*parser.StructuredConfig, error)

ValidateSyntaxAndSchema performs syntax and schema validation on HAProxy configuration.

This function runs Phase 1 (syntax validation) and Phase 1.5 (API schema validation) but NOT Phase 2 (semantic validation with haproxy binary).

Use this when you need to parse the config without needing file I/O or the haproxy binary. The primary use case is when you need to parse the original config (before path modifications) for downstream reuse, while semantic validation is done separately with a modified config.

Parameters:

  • config: The HAProxy configuration content to validate
  • version: HAProxy/DataPlane API version for schema selection (nil uses default v3.0)

Returns:

  • *parser.StructuredConfig: The parsed configuration
  • error: ValidationError with phase information if validation fails

Types

type AppliedOperation

type AppliedOperation struct {
	// Type is the operation type: "create", "update", or "delete"
	Type string

	// Section is the configuration section: "backend", "server", "frontend", "acl", "http-rule", etc.
	Section string

	// Resource is the resource name or identifier (e.g., backend name, server name)
	Resource string

	// Description is a human-readable description of what was changed
	Description string
}

AppliedOperation represents a single applied configuration change.

type AuxiliaryFiles

type AuxiliaryFiles struct {
	// GeneralFiles contains general-purpose files (error pages, custom response files, etc.)
	GeneralFiles []auxiliaryfiles.GeneralFile

	// SSLCertificates contains SSL certificates to sync to HAProxy SSL storage
	SSLCertificates []auxiliaryfiles.SSLCertificate

	// SSLCaFiles contains SSL CA certificate files for client/backend certificate verification
	SSLCaFiles []auxiliaryfiles.SSLCaFile

	// MapFiles contains map files for backend routing and other map-based features
	MapFiles []auxiliaryfiles.MapFile

	// CRTListFiles contains crt-list files for SSL certificate lists with per-certificate options
	CRTListFiles []auxiliaryfiles.CRTListFile
}

AuxiliaryFiles contains files to synchronize alongside configuration changes. Diffs are applied in two of the three sync phases (see SyncPhase in phases.go):

  • PhasePreConfig: creates and updates (files must exist before config references them)
  • PhasePostConfig: deletes (safe to remove only after config stops referencing them)

func DefaultAuxiliaryFiles

func DefaultAuxiliaryFiles() *AuxiliaryFiles

DefaultAuxiliaryFiles returns an empty auxiliary files struct.

func (*AuxiliaryFiles) Sort

func (a *AuxiliaryFiles) Sort()

Sort sorts all auxiliary file slices in-place by their path/filename. This establishes a deterministic order so that downstream consumers (checksum, diff, etc.) can iterate directly without cloning or sorting.

type Capabilities

type Capabilities = client.Capabilities

Capabilities defines which features are available for a given HAProxy/DataPlane API version. This type is re-exported from pkg/dataplane/client for convenience.

func CapabilitiesFromVersion

func CapabilitiesFromVersion(v *Version) Capabilities

CapabilitiesFromVersion computes capabilities based on a HAProxy version. This is used for local HAProxy binary detection (haproxy -v).

Capability thresholds (verified against OpenAPI specs):

  • SupportsCrtList: v3.2+ (CRT-list storage endpoint)
  • SupportsMapStorage: v3.0+ (Map file storage endpoint)
  • SupportsGeneralStorage: v3.0+ (General file storage)
  • SupportsSslCaFiles: v3.2+ (SSL CA file runtime endpoint)
  • SupportsSslCrlFiles: v3.2+ (SSL CRL file runtime endpoint)
  • SupportsLogProfiles: v3.1+ (Log profiles configuration endpoint)
  • SupportsTraces: v3.1+ (Traces configuration endpoint)
  • SupportsAcmeProviders: v3.2+ (ACME provider configuration endpoint)
  • SupportsQUIC: v3.0+ (QUIC/HTTP3 configuration options)
  • SupportsQUICInitialRules: v3.1+ (QUIC initial rules endpoints)
  • SupportsHTTP2: v3.0+ (HTTP/2 configuration)
  • SupportsRuntimeMaps: v3.0+ (Runtime map operations)
  • SupportsRuntimeServers: v3.0+ (Runtime server operations)

type Client

type Client struct {
	// Endpoint contains connection information
	Endpoint Endpoint
	// contains filtered or unexported fields
}

Client manages a persistent connection to the HAProxy Dataplane API. It reuses connections for multiple operations, making it efficient for repeated sync operations.

For production use with multiple operations, create a Client explicitly:

client, err := dataplane.NewClient(ctx, endpoint)
if err != nil {
    return err
}
defer client.Close()

// Reuse client for multiple operations
result1, err := client.Sync(ctx, config1, auxFiles1, opts)
result2, err := client.Sync(ctx, config2, auxFiles2, opts)

func NewClient

func NewClient(ctx context.Context, endpoint *Endpoint) (*Client, error)

NewClient creates a new Client for the given endpoint. The client reuses connections for multiple operations.

Example:

// NewClient takes endpoint by pointer (so the controller can mutate
// the cached version fields on the same struct).
endpoint := &dataplane.Endpoint{
    URL:      "http://haproxy:5555/v3",
    Username: "admin",
    Password: "secret",
}

client, err := dataplane.NewClient(ctx, endpoint)
if err != nil {
    return fmt.Errorf("creating client: %w", err)
}
defer client.Close()

result, err := client.Sync(ctx, desiredConfig, nil, nil)

func (*Client) Close

func (c *Client) Close() error

Close releases client resources. The current implementation has no background work to clean up, but the method is part of the documented API so existing `defer client.Close()` call sites stay valid as the client gains owned resources.

func (*Client) Sync

func (c *Client) Sync(ctx context.Context, desiredConfig string, auxFiles *AuxiliaryFiles, opts *SyncOptions) (*SyncResult, error)

Sync synchronizes the desired HAProxy configuration using this client.

This method:

  1. Fetches the current configuration from the Dataplane API
  2. Parses both current and desired configurations
  3. Compares them to compute a fine-grained ConfigDiff, classifying changes as runtime-eligible server-field updates vs. structural changes
  4. Applies the desired configuration with a single full-config push (no per-operation transactions): a skip-reload raw push carrying X-Runtime-Actions when every change is runtime-eligible, otherwise a force-reload raw push
  5. Retries transient connection failures across HAProxy's reload re-exec
  6. Returns detailed results including applied changes and reload information

Parameters:

  • ctx: Context for cancellation and timeout
  • desiredConfig: The desired HAProxy configuration as a string
  • auxFiles: Auxiliary files to sync (use nil for defaults)
  • opts: Sync options (use nil for defaults)

Returns:

  • *SyncResult: Detailed information about the sync operation
  • error: Detailed error with actionable hints if the sync fails

Example:

client, err := dataplane.NewClient(ctx, endpoint)
if err != nil {
    return err
}
defer client.Close()

result, err := client.Sync(ctx, desiredConfig, nil, nil)
if err != nil {
    return fmt.Errorf("sync failed: %w", err)
}

fmt.Printf("Applied %d operations in %v\n", len(result.AppliedOperations), result.Duration)

func (*Client) SyncRuntimeFast

func (c *Client) SyncRuntimeFast(ctx context.Context, updates *RuntimeServerUpdates, desiredConfig string, opts *SyncOptions) (*SyncResult, error)

SyncRuntimeFast is the runtime-raw apply: a single raw config push of the DESIRED config body with skip_reload+skip_version, carrying the precomputed render diff's runtime `set server` actions (X-Runtime-Actions). No per-pod fetch and no reload — cost is O(config size) for the push but independent of the number of pods (the diff is computed once and shared across pods).

updates is the precomputed runtime-eligible render diff (ComputeRuntimeServerUpdates); desiredConfig is the render whose server addresses/states the actions move the live worker to. Callers: the deployer's runtime-raw lane (a purely runtime-eligible render — this is the only apply), and the scheduler's pre-interval apply (the runtime subset of a STRUCTURAL render, applied off the interval-gated path before the gated reload). When the body carries structural changes they land on disk un-activated until that gated reload — never hidden from a reload indefinitely. Reloads are config-driven (no server-state-file — ADR-0011), so the change persists across any later structural reload because that deploy re-renders the current endpoints.

type ConfigParser

type ConfigParser interface {
	ParseFromString(config string) (*parserconfig.StructuredConfig, error)
}

ConfigParser defines the interface for HAProxy configuration parsing. Both CE (parser.Parser) and EE (enterprise.Parser) parsers implement this interface.

type ConnectionError

type ConnectionError struct {
	// Endpoint is the URL that failed to connect
	Endpoint string

	// Cause is the underlying connection error
	Cause error
}

ConnectionError represents a failure to connect to the Dataplane API.

func (*ConnectionError) Error

func (e *ConnectionError) Error() string

Error implements the error interface.

func (*ConnectionError) Unwrap

func (e *ConnectionError) Unwrap() error

Unwrap returns the underlying cause for error unwrapping.

type DiffDetails

type DiffDetails struct {
	// Total operation counts
	TotalOperations int
	Creates         int
	Updates         int
	Deletes         int

	// Global and defaults changes
	GlobalChanged   bool
	DefaultsChanged bool

	// Frontend changes
	FrontendsAdded    []string
	FrontendsModified []string
	FrontendsDeleted  []string

	// Backend changes
	BackendsAdded    []string
	BackendsModified []string
	BackendsDeleted  []string

	// Backend diff fields: for each modified backend, the BackendBase fields that differ.
	// Only populated when backend attribute changes (not nested collections) cause the update.
	BackendDiffFields map[string][]string

	// Server changes (map of backend -> server names)
	ServersAdded    map[string][]string
	ServersModified map[string][]string
	ServersDeleted  map[string][]string

	// Auxiliary file changes
	MapsAdded            int
	MapsModified         int
	MapsDeleted          int
	SSLCertsAdded        int
	SSLCertsModified     int
	SSLCertsDeleted      int
	SSLCaFilesAdded      int
	SSLCaFilesModified   int
	SSLCaFilesDeleted    int
	GeneralFilesAdded    int
	GeneralFilesModified int
	GeneralFilesDeleted  int
}

DiffDetails contains detailed diff information about configuration changes.

func NewDiffDetails

func NewDiffDetails() DiffDetails

NewDiffDetails creates an empty DiffDetails with initialized maps.

func (*DiffDetails) String

func (d *DiffDetails) String() string

String returns a human-readable summary of the diff details.

type Endpoint

type Endpoint struct {
	// URL is the Dataplane API endpoint (e.g., "http://haproxy:5555/v3").
	// A trailing "/v2" or "/v3" is stripped before the v3 version-detection probe;
	// only v3.x operations are supported (no v2-only endpoints).
	URL string

	// Username for basic authentication
	Username string

	// Password for basic authentication
	Password string

	// PodName is the Kubernetes pod name (for observability)
	PodName string

	// PodNamespace is the Kubernetes pod namespace (for observability)
	PodNamespace string

	// Version info (cached after discovery admission, avoids redundant /v3/info calls)
	// Zero values indicate version not yet detected.
	DetectedMajorVersion int    // Major version (e.g., 3)
	DetectedMinorVersion int    // Minor version (e.g., 2)
	DetectedFullVersion  string // Full version string (e.g., "v3.2.6 87ad0bcf")
}

Endpoint represents HAProxy Dataplane API connection information.

func (*Endpoint) HasCachedVersion

func (e *Endpoint) HasCachedVersion() bool

HasCachedVersion returns true if version info has been cached on this endpoint.

type HAProxyExecutor

type HAProxyExecutor interface {
	// Version returns the combined output of `haproxy -v`.
	Version() (string, error)
	// Check runs `haproxy <args...>` with the working directory set to
	// workDir and returns the combined output. The working directory
	// matters: semantic validation invokes haproxy with a relative config
	// path so that relative file references inside the config resolve.
	Check(workDir string, args ...string) ([]byte, error)
}

HAProxyExecutor abstracts execution of the local HAProxy binary so that unit tests never shell out to an external process. Production code uses the default binaryHAProxyExecutor; tests install a fake via SetHAProxyExecutor (most conveniently through pkg/dataplane/dataplanetest.InstallFakeHAProxy in a TestMain).

type ParseError

type ParseError struct {
	// ConfigType indicates which config failed: "current" or "desired"
	ConfigType string

	// ConfigSnippet contains the first 200 characters of the problematic config
	ConfigSnippet string

	// Cause is the underlying parsing error
	Cause error
}

ParseError represents a configuration parsing failure.

func (*ParseError) Error

func (e *ParseError) Error() string

Error implements the error interface.

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

Unwrap returns the underlying cause for error unwrapping.

type PathConfig

type PathConfig struct {
	// MapsDir is the base path for HAProxy map files (e.g., /etc/haproxy/maps).
	MapsDir string

	// SSLDir is the base path for HAProxy SSL certificates (e.g., /etc/haproxy/ssl).
	SSLDir string

	// GeneralDir is the base path for HAProxy general files (e.g., /etc/haproxy/general).
	GeneralDir string

	// ConfigFile is the path to the HAProxy configuration file (e.g., /tmp/haproxy.cfg).
	// Only used in validation contexts; can be empty for production paths.
	ConfigFile string
}

PathConfig contains the base directory configuration for HAProxy auxiliary files. These are the raw filesystem paths before capability-based resolution.

type ResolvedPaths

type ResolvedPaths struct {
	// MapsDir is the resolved path for HAProxy map files.
	MapsDir string

	// SSLDir is the resolved path for HAProxy SSL certificates.
	SSLDir string

	// CRTListDir is the resolved path for CRT-list files.
	// Always equals GeneralDir because CRT-list files are stored as general files
	// to avoid reload on create (native CRT-list API doesn't support skip_reload).
	CRTListDir string

	// GeneralDir is the resolved path for HAProxy general files.
	GeneralDir string

	// ConfigFile is the path to the HAProxy configuration file.
	ConfigFile string
}

ResolvedPaths contains capability-aware resolved paths for HAProxy auxiliary files. This is the result of applying capability-based resolution to a PathConfig.

The key difference from PathConfig is that CRTListDir is always set to GeneralDir because CRT-list files are stored as general files to avoid reload on create.

func ResolvePaths

func ResolvePaths(base PathConfig) *ResolvedPaths

ResolvePaths applies capability-based path resolution to base paths. This is the SINGLE SOURCE OF TRUTH for all capability-dependent path logic.

Currently handles:

  • CRT-list storage: Always uses general directory because the native CRT-list API triggers reload on create without supporting skip_reload parameter. General file storage returns 201 without triggering reload.

func (*ResolvedPaths) ToValidationPaths

func (r *ResolvedPaths) ToValidationPaths() *ValidationPaths

ToValidationPaths converts ResolvedPaths to ValidationPaths. Use this when you need ValidationPaths for HAProxy configuration validation. TempDir is derived from ConfigFile's parent directory (e.g., /tmp/validate-xxx/haproxy.cfg -> /tmp/validate-xxx).

type RuntimeServerUpdates

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

RuntimeServerUpdates is the precomputed result of diffing the previous render against the current one for runtime-eligible server changes. The render diff (comparator.Compare, O(config size)) is render-vs-render and so identical for every pod; callers compute it ONCE per fire via ComputeRuntimeServerUpdates and apply it to every pod, instead of re-diffing per pod.

func ComputeRuntimeServerUpdates

func ComputeRuntimeServerUpdates(prev, current *parser.StructuredConfig) (*RuntimeServerUpdates, error)

ComputeRuntimeServerUpdates diffs prev (last-dispatched render) against current (this render) for runtime-eligible server changes. It is a pure, pod- independent computation (no client, no fetch), so the deployer's fast path runs it ONCE per render and applies the result to every pod. Returns a non-nil result even when there are no changes.

func (*RuntimeServerUpdates) IsRuntimeEligible

func (u *RuntimeServerUpdates) IsRuntimeEligible() bool

IsRuntimeEligible reports whether this diff can be applied entirely through the runtime fast path: it carries at least one runtime-eligible server change and NO structural (reload-inducing) operation. A diff with structural ops, or with nothing at all to apply, is not runtime-eligible (the deployer routes it to the rate-limited structural lane).

func (*RuntimeServerUpdates) ServerOpCount

func (u *RuntimeServerUpdates) ServerOpCount() int

ServerOpCount returns the number of runtime-eligible server changes in the set (0 = nothing to apply). Safe on a nil receiver.

func (*RuntimeServerUpdates) StructuralOpCount

func (u *RuntimeServerUpdates) StructuralOpCount() int

StructuralOpCount returns the number of non-runtime-eligible (reload-inducing) operations in the render diff. Safe on a nil receiver (returns 0). The deployer's lane classifier uses this to decide runtime-raw vs structural: a diff with zero structural ops can apply purely via the runtime fast path (no reload), bypassing the deployment interval.

type SyncError

type SyncError struct {
	// Stage indicates where the failure occurred. Common values:
	//   "connect", "parse-current", "parse-desired",
	//   "compare", "compare_files", "compare_ssl", "compare_ssl_ca", "compare_maps", "compare_crtlists",
	//   "sync_ssl_pre", "sync_ssl_ca_pre", "sync_files_pre", "sync_maps_pre",
	//   "apply", "commit", "reload_verification", "auxiliary_reload_verification",
	//   "fallback"
	Stage string

	// Message provides a detailed error description
	Message string

	// Cause is the underlying error that caused the failure
	Cause error

	// Hints provides actionable suggestions for fixing the problem
	Hints []string
}

SyncError represents a synchronization failure with actionable context. It provides detailed information about what stage failed and suggestions for how to fix the problem.

func NewConnectionError

func NewConnectionError(endpoint string, cause error) *SyncError

NewConnectionError creates a ConnectionError.

func NewParseError

func NewParseError(configType, configSnippet string, cause error) *SyncError

NewParseError creates a ParseError.

func (*SyncError) Error

func (e *SyncError) Error() string

Error implements the error interface.

func (*SyncError) Unwrap

func (e *SyncError) Unwrap() error

Unwrap returns the underlying cause for error unwrapping.

type SyncMode

type SyncMode string

SyncMode indicates which sync strategy was used.

const (
	// SyncModeNoChanges indicates the diff was empty; no API calls were made.
	SyncModeNoChanges SyncMode = "no_changes"
	// SyncModeRuntime indicates the runtime path: one PushRawConfigurationSkipReload
	// with X-Runtime-Actions, no HAProxy reload.
	SyncModeRuntime SyncMode = "runtime"
	// SyncModeReload indicates the reload path: optional skip_reload+actions push
	// to seed the live worker, then force_reload.
	SyncModeReload SyncMode = "reload"
)

type SyncOptions

type SyncOptions struct {
	// Timeout for the entire sync operation (default: 2 minutes)
	Timeout time.Duration

	// VerifyReload enables async reload verification after sync (default: true)
	// When true, polls the reload status endpoint until succeeded/failed/timeout.
	// Disable for dry-run or when reload verification is not needed.
	VerifyReload bool

	// ReloadVerificationTimeout is the maximum time to wait for reload verification (default: 10s)
	// This should be set higher than the DataPlane API's reload-delay setting.
	// Only used when VerifyReload is true.
	ReloadVerificationTimeout time.Duration

	// PreParsedConfig is an optional pre-parsed desired configuration. When
	// non-nil, sync skips parsing the desiredConfig string.
	PreParsedConfig *parserconfig.StructuredConfig

	// CachedCurrentConfig is an optional cached parsed current configuration
	// from a previous sync. When set with CachedConfigVersion, sync calls
	// GetVersion() first and reuses the cached config if the version matches.
	CachedCurrentConfig *parserconfig.StructuredConfig

	// CachedConfigVersion is the expected config version on the pod. Only
	// used when CachedCurrentConfig is also set.
	CachedConfigVersion int64

	// ContentChecksum is the pre-computed checksum of the desired config +
	// aux files. When it matches LastDeployedChecksum, the expensive aux
	// file comparison is skipped because the desired state is identical
	// to what was last deployed.
	ContentChecksum string

	// LastDeployedChecksum is the content checksum from the last successful
	// sync to this endpoint. Drift prevention syncs leave this empty.
	LastDeployedChecksum string

	// RestampVersionHeader (SyncRuntimeFast only) re-writes the pushed body
	// WITH a `# _version=N` header after a successful pure-runtime apply, via
	// one versioned skip_reload push. A skip_version push leaves the on-disk
	// config headerless, and sync() refuses to trust an empty diff against a
	// headerless config (it forces a reload to activate potentially parked
	// content). Re-stamping proves "disk == running state" so the next
	// structural sync stays reload-free.
	//
	// Callers must set this ONLY when no structural deploy can be in flight
	// on the pod (the deployer's authoritative runtime-raw lane dispatch).
	// A fast-track partial apply racing an in-flight structural reload must
	// leave the config headerless: its `set server` actions can land on the
	// worker the reload replaces, and a re-stamped header would let the next
	// sync trust an empty diff over that lost update.
	RestampVersionHeader bool
}

SyncOptions configures synchronization behavior.

func DefaultSyncOptions

func DefaultSyncOptions() *SyncOptions

DefaultSyncOptions returns sensible default sync options.

type SyncPhase

type SyncPhase int

SyncPhase identifies one of the three phases of a configuration sync.

HAProxy configs can reference auxiliary files (maps, SSL certs, error pages, etc.). A single sync therefore runs in three phases, sequenced so that referenced files exist before the config mentions them and orphaned files are only cleaned up after the config that stopped referencing them has been applied:

  1. PhasePreConfig — create or update auxiliary files. Includes verifying any reloads those operations trigger, so the next phase's config operations don't race against pending reloads.
  2. PhaseConfig — apply HAProxy configuration changes by pushing the full rendered config (raw push), reloading only when structural changes are present.
  3. PhasePostConfig — delete auxiliary files that the new config no longer references. Must run after PhaseConfig succeeds.

The enum is advisory — used in log fields and error context — rather than a control-flow driver; the orchestrator still calls the phase-specific helpers directly.

const (
	// PhasePreConfig syncs auxiliary files BEFORE the HAProxy config that
	// references them.
	PhasePreConfig SyncPhase = iota + 1

	// PhaseConfig applies the HAProxy configuration change.
	PhaseConfig

	// PhasePostConfig deletes auxiliary files that are no longer referenced.
	PhasePostConfig
)

func (SyncPhase) String

func (p SyncPhase) String() string

String returns a human-readable phase name for logging.

type SyncResult

type SyncResult struct {
	// Success indicates whether the sync completed successfully
	Success bool

	// AppliedOperations contains structured information about operations that were applied
	AppliedOperations []AppliedOperation

	// ReloadTriggered indicates whether a HAProxy reload was triggered
	// true when commit status is 202, false when 200
	ReloadTriggered bool

	// ReloadID is the reload identifier from the Reload-ID response header
	// Only set when ReloadTriggered is true
	ReloadID string

	// ReloadVerified indicates whether the reload was verified as successful.
	// Only set when VerifyReload option is enabled and ReloadTriggered is true.
	ReloadVerified bool

	// ReloadVerificationError contains an error message if reload verification failed.
	// This includes timeout errors or explicit reload failures from HAProxy.
	ReloadVerificationError string

	// SyncMode indicates which sync strategy was used.
	// See SyncMode* constants for possible values.
	SyncMode SyncMode

	// Duration of the sync operation
	Duration time.Duration

	// Details contains detailed diff information
	// This field is always populated regardless of SyncMode
	Details DiffDetails

	// Message provides additional context about the result
	Message string

	// PostSyncVersion is the config version on the pod after a successful sync.
	// Callers can cache this alongside the desired parsed config to skip
	// redundant GetRawConfiguration() + parse on subsequent syncs when the
	// pod's version hasn't changed. Zero means version was not captured.
	//
	// Never carries 1: version 1 is the headerless sentinel — a config
	// written by a skip_version push (the runtime bypass) has no
	// `# _version=N` header and GetVersion reads it as 1 regardless of
	// body, so 1 cannot discriminate states and must never be cached
	// (see fetchCurrentConfig). The runtime fast path (SyncRuntimeFast)
	// deliberately leaves PostSyncVersion zero and PostSyncParsedConfig
	// nil: it is fetch-free by design, and its skip_version push strips
	// the version header, forcing the next versioned sync to take a
	// cache miss and re-fetch the pod's actual state.
	PostSyncVersion int64

	// PostSyncParsedConfig is the pod's actual configuration AFTER the sync
	// completed, fetched and parsed from the dataplane API. Set only when
	// AppliedOperations is non-empty AND the post-sync fetch+parse
	// succeeded; nil otherwise (no-changes paths, or fetch/parse failure —
	// callers fall back to the desired config in those cases).
	//
	// Callers should cache this in preference to their input desired config:
	// incremental dataplane patches don't guarantee byte-identity with the
	// caller's intent (different starting baselines across pods produce
	// logically-equivalent-but-byte-different end states). Caching the
	// actual post-sync state lets subsequent drift checks compare apples to
	// apples — the comparator sees pod-actual vs desired, not
	// desired vs desired.
	PostSyncParsedConfig *parserconfig.StructuredConfig
}

SyncResult contains detailed information about a sync operation.

func (*SyncResult) String

func (r *SyncResult) String() string

String returns a human-readable summary of the sync result.

type ValidationError

type ValidationError struct {
	// Phase indicates which validation phase failed: "syntax" or "semantic"
	Phase string

	// Message is the validation error message
	Message string

	// Cause is the underlying error
	Cause error
}

ValidationError represents semantic validation failure from HAProxy.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface.

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

Unwrap returns the underlying error for error unwrapping.

type ValidationPaths

type ValidationPaths struct {
	// TempDir is the root temp directory for validation files.
	// The validator is responsible for cleaning this up after validation completes.
	// This prevents race conditions where the renderer's cleanup runs before
	// the async validator can use the validation files.
	TempDir           string
	MapsDir           string
	SSLCertsDir       string
	CRTListDir        string // Directory for CRT-list files (may differ from SSLCertsDir on HAProxy < 3.2)
	GeneralStorageDir string
	ConfigFile        string
}

ValidationPaths holds the filesystem paths for HAProxy validation. These paths must match the HAProxy Dataplane API server's resource configuration.

type Version

type Version = client.Version

Version is the project-wide HAProxy / DataPlane API version type. It is an alias of client.Version (Major.Minor.Patch with Compare), kept under the dataplane.Version name so existing callers (discovery, validation, testrunner, …) compile unchanged.

func DetectLocalVersion

func DetectLocalVersion() (*Version, error)

DetectLocalVersion runs "haproxy -v" (via the installed HAProxyExecutor) and returns the local HAProxy version. Returns an error if haproxy is not found or version cannot be parsed.

func ParseHAProxyVersionOutput

func ParseHAProxyVersionOutput(output string) (*Version, error)

ParseHAProxyVersionOutput parses the output of "haproxy -v" command. Expected format: "HAProxy version 3.2.9 2025/11/21 - https://haproxy.org/\n..." Returns extracted major.minor version.

func ParseVersionString

func ParseVersionString(version string) (*Version, error)

ParseVersionString parses a version string like "3.3" or "3.3.0" into a Version. This is useful for parsing user-provided version constraints.

Unlike client.ParseVersion it is intentionally strict about the "v" prefix: a leading "v" is rejected rather than stripped, so an operator typo such as "v3.3.beta" surfaces as a parse error (callers then run the test anyway rather than silently skipping it). The original input is preserved verbatim in Version.Full so callers can echo it back unchanged.

func VersionFromAPIInfo

func VersionFromAPIInfo(info *client.VersionInfo) (*Version, error)

VersionFromAPIInfo converts client.VersionInfo (from /v3/info) to Version. The API version string format is "vX.Y.Z commit" (e.g., "v3.2.6 87ad0bcf").

Directories

Path Synopsis
Package auxiliaryfiles provides functionality for synchronizing auxiliary files (general files, SSL certificates, map files, crt-lists) with the HAProxy Dataplane API.
Package auxiliaryfiles provides functionality for synchronizing auxiliary files (general files, SSL certificates, map files, crt-lists) with the HAProxy Dataplane API.
Package client provides a high-level wrapper around the generated HAProxy Dataplane API client.
Package client provides a high-level wrapper around the generated HAProxy Dataplane API client.
Package comparator provides comparison functions for HAProxy Enterprise Edition sections.
Package comparator provides comparison functions for HAProxy Enterprise Edition sections.
sections
Package sections provides factory functions for creating HAProxy configuration operations.
Package sections provides factory functions for creating HAProxy configuration operations.
Package dataplanetest provides test doubles for pkg/dataplane.
Package dataplanetest provides test doubles for pkg/dataplane.
Package parser provides HAProxy configuration parsing using client-native library.
Package parser provides HAProxy configuration parsing using client-native library.
enterprise
Package enterprise provides HAProxy Enterprise Edition configuration parsing.
Package enterprise provides HAProxy Enterprise Edition configuration parsing.
enterprise/parsers
Package parsers provides wrapper parsers for HAProxy Enterprise Edition directives.
Package parsers provides wrapper parsers for HAProxy Enterprise Edition directives.
parserconfig
Package parserconfig provides canonical configuration types for HAProxy parsing.
Package parserconfig provides canonical configuration types for HAProxy parsing.
sectionextract
Package sectionextract extracts standard (Community Edition) HAProxy sections from a client-native config-parser into a parserconfig.StructuredConfig.
Package sectionextract extracts standard (Community Edition) HAProxy sections from a client-native config-parser into a parserconfig.StructuredConfig.
Package validators provides zero-allocation OpenAPI validation for HAProxy models.
Package validators provides zero-allocation OpenAPI validation for HAProxy models.

Jump to

Keyboard shortcuts

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