client

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

README

pkg/dataplane/client

Low-level multi-version HAProxy Dataplane API client.

Overview

Wraps haproxytech/client-native clients for HAProxy Dataplane API v3.0 / v3.1 / v3.2 / v3.3 and Enterprise variants. The version is auto-detected at construction time; downstream callers reach for Clientset().V33() (etc.) only when they need a version-specific endpoint, otherwise they go through the version-aware helpers in this package.

The package also provides Transaction (a typed wrapper around the Dataplane API's transaction lifecycle) and (*Transaction).Abort to cancel one. The HAPTIC controller no longer uses transactions in its main sync path — it pushes the full rendered config via PushRawConfiguration / PushRawConfigurationSkipReload. CreateTransaction + Transaction.Abort are retained for the enterprise integration tests that drive per-section CRUD endpoints directly; new production callers should use the higher-level entry points in pkg/dataplane instead.

Quick Start

import (
    "context"

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

dpClient, err := client.New(ctx, &client.Config{
    BaseURL:  "http://haproxy-dataplane:5555",
    Username: "admin",
    Password: "password",
})
if err != nil { /* ... */ }

caps := dpClient.Clientset().Capabilities()
if caps.SupportsCrtList { /* HAProxy 3.2+ — use crt-list storage endpoints */ }

// Push a new config (triggers HAProxy reload; use PushRawConfigurationSkipReload
// plus runtime actions when only server addresses changed).
reloadID, err := dpClient.PushRawConfiguration(ctx, newConfig, expectedVersion)

client.New takes a context.Context (used for the version-detection probe) plus a *Config pointer; the config is validated synchronously and BaseURL, Username, and Password are required.

See Also

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package client provides a high-level wrapper around the generated HAProxy Dataplane API client.

This wrapper adds: - Multi-version support (v3.0, v3.1, v3.2, v3.3) and Enterprise variants - Runtime version detection - Capability-based feature detection - Raw config push (skip_reload + X-Runtime-Actions, or force_reload) - Auxiliary file storage operations (maps, SSL certs, general files, crt-lists) - Error handling and retry logic

Package client provides a multi-version wrapper for HAProxy Dataplane API clients.

This package implements the Kubernetes-style clientset pattern to support multiple HAProxy DataPlane API versions (3.0, 3.1, 3.2, 3.3) with: - Runtime version detection using /v3/info endpoint - Capability-based routing for graceful degradation - Version-specific client accessors

Index

Constants

View Source
const ReloadIDHeader = "Reload-Id"

ReloadIDHeader is the HTTP header name used by HAProxy Data Plane API to return the reload ID when an operation triggers a reload.

Variables

View Source
var (
	// ErrCrtListRequiresV32 is returned when crt-list storage operations are attempted
	// on DataPlane API versions below v3.2.
	ErrCrtListRequiresV32 = errors.New("crt-list storage requires DataPlane API v3.2+")

	// ErrSSLCaFilesRequireV32 is returned when SSL CA file operations are attempted
	// on DataPlane API versions below v3.2.
	ErrSSLCaFilesRequireV32 = errors.New("SSL CA file storage requires DataPlane API v3.2+")
)

Functions

func CheckResponse

func CheckResponse(resp *http.Response, operation string) error

CheckResponse validates an HTTP response status code and logs failures with full context. It reads and logs the response body for debugging, then returns a user-friendly error.

Usage:

resp, err := c.Dispatch(ctx, callFunc)
if err != nil {
    return fmt.Errorf("creating backend: %w", err)
}
defer resp.Body.Close()

if err := client.CheckResponse(resp, "create backend"); err != nil {
    return err
}

func IsEnterpriseVersion

func IsEnterpriseVersion(version string) bool

IsEnterpriseVersion returns true when the version string matches one of the known HAProxy Enterprise formats.

func SanitizeSSLCertName

func SanitizeSSLCertName(name string) string

SanitizeSSLCertName sanitizes a certificate name for HAProxy Data Plane API storage. The API replaces dots in the filename (excluding the extension) with underscores. For example: "example.com.pem" becomes "example_com.pem". This function is exported for use in tests to compare certificate names.

func SanitizeStorageName

func SanitizeStorageName(name string) string

SanitizeStorageName sanitizes a filename for HAProxy storage. The API replaces dots in the filename (excluding the extension) with underscores. Example: "example.com.pem" becomes "example_com.pem".

func UnsanitizeStorageName

func UnsanitizeStorageName(name string) string

UnsanitizeStorageName reverses sanitization (best-effort). Converts underscores back to dots in the basename. Example: "example_com.pem" becomes "example.com.pem". Note: This may not be perfect for filenames that originally contained underscores.

func WithRetry

func WithRetry[T any](ctx context.Context, config RetryConfig, fn func(attempt int) (T, error)) (T, error)

WithRetry executes fn with automatic retry logic based on config.

The function fn is called with the current attempt number (1-indexed). If fn returns an error and config.RetryIf returns true, the operation is retried up to config.MaxAttempts times.

Example:

config := RetryConfig{
    MaxAttempts: 3,
    RetryIf:     IsVersionConflict(),
    Backoff:     BackoffExponential,
    BaseDelay:   100 * time.Millisecond,
    Logger:      logger,
}
result, err := WithRetry(ctx, config, func(attempt int) (*Result, error) {
    return doOperation(ctx, attempt)
})

Types

type BackoffStrategy

type BackoffStrategy string

BackoffStrategy determines the delay between retry attempts.

const (
	// BackoffExponential applies exponentially increasing delays between retries.
	// It is the only strategy production uses; any other value (including the
	// zero value) means no delay.
	BackoffExponential BackoffStrategy = "exponential"
)

type CallFunc

type CallFunc[T any] struct {
	// Community edition clients
	// V33 is the function to call for DataPlane API v3.3+
	V33 func(*v33.Client) (T, error)

	// V32 is the function to call for DataPlane API v3.2
	V32 func(*v32.Client) (T, error)

	// V31 is the function to call for DataPlane API v3.1
	V31 func(*v31.Client) (T, error)

	// V30 is the function to call for DataPlane API v3.0
	V30 func(*v30.Client) (T, error)

	// Enterprise edition clients
	// V32EE is the function to call for HAProxy Enterprise DataPlane API v3.2+
	V32EE func(*v32ee.Client) (T, error)

	// V31EE is the function to call for HAProxy Enterprise DataPlane API v3.1
	V31EE func(*v31ee.Client) (T, error)

	// V30EE is the function to call for HAProxy Enterprise DataPlane API v3.0
	V30EE func(*v30ee.Client) (T, error)
}

CallFunc represents a versioned API call function. Each field is a function that takes a version-specific client and returns a result of type T. This allows type-safe dispatch to the appropriate client version based on runtime detection.

For HAProxy Community editions, use V30, V31, V32, V33. For HAProxy Enterprise editions, use V30EE, V31EE, V32EE.

Example usage:

resp, err := c.Dispatch(ctx, CallFunc[*http.Response]{
    V32: func(c *v32.Client) (*http.Response, error) { return c.SomeMethod(ctx, params) },
    V31: func(c *v31.Client) (*http.Response, error) { return c.SomeMethod(ctx, params) },
    V30: func(c *v30.Client) (*http.Response, error) { return c.SomeMethod(ctx, params) },
    V32EE: func(c *v32ee.Client) (*http.Response, error) { return c.SomeMethod(ctx, params) },
    V31EE: func(c *v31ee.Client) (*http.Response, error) { return c.SomeMethod(ctx, params) },
    V30EE: func(c *v30ee.Client) (*http.Response, error) { return c.SomeMethod(ctx, params) },
})

type Capabilities

type Capabilities struct {
	// Storage capabilities
	SupportsCrtList         bool // /v3/storage/ssl_crt_lists (v3.2+)
	SupportsMapStorage      bool // /v3/storage/maps (v3.0+)
	SupportsGeneralStorage  bool // /v3/storage/general (v3.0+)
	SupportsSslCaFiles      bool // /v3/runtime/ssl_ca_files (v3.2+)
	SupportsSslCrlFiles     bool // /v3/runtime/ssl_crl_files (v3.2+)
	SupportsRuntimeSSLCerts bool // /v3/runtime/ssl_certs replaceCert (v3.2+)

	// Configuration capabilities
	SupportsHTTP2            bool // HTTP/2 configuration (v3.0+)
	SupportsQUIC             bool // QUIC/HTTP3 configuration (v3.0+)
	SupportsQUICInitialRules bool // QUIC initial rules endpoints (v3.1+)

	// Observability capabilities
	SupportsLogProfiles bool // /v3/services/haproxy/configuration/log_profiles (v3.1+)
	SupportsTraces      bool // /v3/services/haproxy/configuration/traces (v3.1+)

	// Certificate automation capabilities
	SupportsAcmeProviders bool // /v3/services/haproxy/configuration/acmes (v3.2+)

	// Model metadata capabilities
	SupportsConfigMetadata bool // Metadata field on config models like ACL, Server, etc. (v3.2+)

	// Runtime capabilities
	SupportsRuntimeMaps    bool // Runtime map operations (v3.0+)
	SupportsRuntimeServers bool // Runtime server operations (v3.0+)

	// SupportsWAF indicates WAF management endpoints are available.
	// Includes: waf_body_rules (frontend/backend), waf/rulesets
	// Note: waf_global and waf_profiles require v3.2+ (see SupportsWAFGlobal, SupportsWAFProfiles)
	SupportsWAF bool

	// SupportsWAFGlobal indicates WAF global configuration endpoint is available.
	// Only available in HAProxy Enterprise v3.2+ (waf_global endpoint)
	SupportsWAFGlobal bool

	// SupportsWAFProfiles indicates WAF profile management endpoints are available.
	// Only available in HAProxy Enterprise v3.2+ (waf_profiles endpoint)
	SupportsWAFProfiles bool

	// SupportsUDPLBACLs indicates UDP load balancer ACL endpoints are available.
	// Only available in HAProxy Enterprise v3.2+ (udp_lbs/{name}/acls endpoint)
	SupportsUDPLBACLs bool

	// SupportsUDPLBServerSwitchingRules indicates UDP load balancer server switching rule endpoints are available.
	// Only available in HAProxy Enterprise v3.2+ (udp_lbs/{name}/server_switching_rules endpoint)
	SupportsUDPLBServerSwitchingRules bool

	// SupportsKeepalived indicates Keepalived/VRRP management endpoints are available.
	// Includes: vrrp_instances, vrrp_sync_groups, vrrp_track_scripts, keepalived transactions
	SupportsKeepalived bool

	// SupportsUDPLoadBalancing indicates UDP load balancer management endpoints are available.
	// Includes: udp_lbs with ACLs, dgram_binds, log_targets, server_switching_rules
	SupportsUDPLoadBalancing bool

	// SupportsBotManagement indicates bot management endpoints are available.
	// Includes: botmgmt_profiles, captchas
	SupportsBotManagement bool

	// SupportsGitIntegration indicates Git integration endpoints are available.
	// Includes: git/settings, git/actions
	SupportsGitIntegration bool

	// SupportsDynamicUpdate indicates dynamic update endpoints are available.
	// Includes: dynamic_update_rules, dynamic_update_section
	SupportsDynamicUpdate bool

	// SupportsALOHA indicates ALOHA feature endpoints are available.
	// Includes: aloha, aloha/actions
	SupportsALOHA bool

	// SupportsAdvancedLogging indicates advanced logging endpoints are available.
	// Includes: logs/config, logs/inputs, logs/outputs
	SupportsAdvancedLogging bool

	// SupportsPing indicates the ping endpoint is available.
	// Only available in HAProxy Enterprise v3.2+ (/v3/ping endpoint)
	SupportsPing bool
}

Capabilities defines which features are available for a given DataPlane API version. Version thresholds verified against OpenAPI specs for v3.0, v3.1, v3.2, v3.3.

type Clientset

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

Clientset provides access to clients for all supported HAProxy DataPlane API versions. This follows the Kubernetes clientset pattern, allowing version-specific operations while maintaining compatibility across HAProxy versions.

func NewClientset

func NewClientset(ctx context.Context, endpoint *Endpoint, logger *slog.Logger) (*Clientset, error)

NewClientset creates a new multi-version clientset for the given endpoint. It detects the server's DataPlane API version and creates appropriate clients.

Example:

// endpoint is taken by pointer so &client.Endpoint{...} is required.
clientset, err := client.NewClientset(ctx, &client.Endpoint{
    URL:      "http://haproxy:5555",
    Username: "admin",
    Password: "password",
}, logger)
if err != nil {
    return err
}

// Use version-specific client. Naming the variable `versioned`
// avoids shadowing the imported `client` package.
if clientset.Capabilities().SupportsCrtList {
    versioned := clientset.V32()
    // Use v3.2-specific features
    _ = versioned
} else {
    versioned := clientset.V30()
    // Fallback to v3.0-compatible operations
    _ = versioned
}

func (*Clientset) Capabilities

func (c *Clientset) Capabilities() Capabilities

Capabilities returns the feature availability map for the detected version.

func (*Clientset) DetectedVersion

func (c *Clientset) DetectedVersion() string

DetectedVersion returns the full version string detected from the server. Example: "v3.2.6 87ad0bcf" for community or "v3.0r1" for enterprise.

func (*Clientset) IsEnterprise

func (c *Clientset) IsEnterprise() bool

IsEnterprise returns true if the detected HAProxy is an Enterprise edition.

func (*Clientset) MajorVersion

func (c *Clientset) MajorVersion() int

MajorVersion returns the major version number (e.g., 3 for v3.x).

func (*Clientset) MinorVersion

func (c *Clientset) MinorVersion() int

MinorVersion returns the minor version number (e.g., 0, 1, or 2 for v3.0, v3.1, v3.2).

func (*Clientset) PreferredClient

func (c *Clientset) PreferredClient() any

PreferredClient returns the most appropriate client based on detected version and edition. This is useful for code that wants to use the best available API without explicitly checking capabilities.

Returns:

  • Enterprise clients (v32ee, v31ee, v30ee) if HAProxy Enterprise is detected
  • Community clients (v32, v31, v30) for HAProxy Community

Version selection (newer minors clamp down to the newest bundled client):

  • v3.3 client if server is v3.3 or newer
  • v3.2 client if server is v3.2
  • v3.1 client if server is v3.1
  • v3.0 client if server is v3.0 or unknown

func (*Clientset) V30

func (c *Clientset) V30() *v30.Client

V30 returns the DataPlane API v3.0 client. This client is compatible with HAProxy 2.4 and later.

func (*Clientset) V30EE

func (c *Clientset) V30EE() *v30ee.Client

V30EE returns the HAProxy Enterprise DataPlane API v3.0 client.

func (*Clientset) V31

func (c *Clientset) V31() *v31.Client

V31 returns the DataPlane API v3.1 client. This client is compatible with HAProxy 2.6 and later.

func (*Clientset) V31EE

func (c *Clientset) V31EE() *v31ee.Client

V31EE returns the HAProxy Enterprise DataPlane API v3.1 client.

func (*Clientset) V32

func (c *Clientset) V32() *v32.Client

V32 returns the DataPlane API v3.2 client. This client is compatible with HAProxy 2.8 and later.

func (*Clientset) V32EE

func (c *Clientset) V32EE() *v32ee.Client

V32EE returns the HAProxy Enterprise DataPlane API v3.2 client.

func (*Clientset) V33

func (c *Clientset) V33() *v33.Client

V33 returns the DataPlane API v3.3 client.

type Config

type Config struct {
	// BaseURL is the HAProxy Dataplane API endpoint (e.g., "http://localhost: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).
	BaseURL string

	// Username for basic authentication
	Username string

	// Password for basic authentication
	Password string

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

	// HTTPClient allows injecting a custom HTTP client (useful for testing)
	HTTPClient *http.Client

	// Logger for logging request/response details on errors (optional)
	// If nil, slog.Default() will be used
	Logger *slog.Logger
}

Config contains configuration options for creating a DataplaneClient.

type DataplaneClient

type DataplaneClient struct {
	Endpoint Endpoint // Embedded endpoint information
	// contains filtered or unexported fields
}

DataplaneClient wraps the multi-version Clientset with additional functionality for HAProxy Dataplane API operations. It automatically uses the appropriate client version based on runtime detection.

func New

func New(ctx context.Context, cfg *Config) (*DataplaneClient, error)

New creates a new DataplaneClient with the provided configuration. It automatically detects the server's DataPlane API version and creates an appropriate multi-version clientset.

Example:

// dpClient avoids shadowing the imported `client` package; cfg is
// taken by pointer so &client.Config{...} is required.
dpClient, err := client.New(ctx, &client.Config{
    BaseURL:  "http://haproxy-dataplane:5555",
    Username: "admin",
    Password: "password",
})

func NewFromEndpoint

func NewFromEndpoint(ctx context.Context, endpoint *Endpoint, logger *slog.Logger) (*DataplaneClient, error)

NewFromEndpoint creates a new DataplaneClient from an Endpoint. This is a convenience function for creating a client with default options.

func (*DataplaneClient) AddRuntimeCaFileEntry

func (c *DataplaneClient) AddRuntimeCaFileEntry(ctx context.Context, name, content string) error

AddRuntimeCaFileEntry replaces the live (in-memory) contents of an existing, config-referenced CA file via the runtime add-entry endpoint (`add ssl ca-file` + `commit ssl ca-file`), WITHOUT a reload. With no ongoing transaction, `add ssl ca-file` starts an EMPTY transaction and commit replaces the file with the payload — so passing the full desired bundle replaces the live CA file (verified: a 2-cert bundle replaces a 1-cert file, no reload). v3.2+ only.

HAPTIC uses this instead of UpdateSSLCaFile (set ssl ca-file): the DataPlane API's `set ssl ca-file` runtime path returns 500 under the master-worker `set severity-output number;@1 <heredoc>;quit` wrapping — the slower CA validation races the connection close and its response is lost. `add ssl ca-file` wins that race and applies reliably. (Upstream client-native quirk; see the runtime-socket reduction audit memo.)

func (*DataplaneClient) BaseURL

func (c *DataplaneClient) BaseURL() string

BaseURL returns the configured base URL for the Dataplane API.

func (*DataplaneClient) Capabilities

func (c *DataplaneClient) Capabilities() Capabilities

Capabilities returns the feature availability for the detected version.

func (*DataplaneClient) Clientset

func (c *DataplaneClient) Clientset() *Clientset

Clientset returns the underlying multi-version clientset for advanced operations. Use this when you need version-specific features or capability checking.

Example:

if client.Clientset().Capabilities().SupportsCrtList {
    v32Client := client.Clientset().V32()
    // Use v3.2-specific crt-list operations
}

func (*DataplaneClient) CreateCRTListFile

func (c *DataplaneClient) CreateCRTListFile(ctx context.Context, name, content string) (string, error)

CreateCRTListFile creates a new crt-list file using multipart form-data. Returns the reload ID if a reload was triggered (empty string if not) and any error. The name parameter can use dots (e.g., "example.com.crtlist"), which will be sanitized automatically before calling the API. CRT-list storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) CreateGeneralFile

func (c *DataplaneClient) CreateGeneralFile(ctx context.Context, path, content string) (string, error)

CreateGeneralFile creates a new general file using multipart form-data. Returns the reload ID if a reload was triggered (empty string if not) and any error. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) CreateMapFile

func (c *DataplaneClient) CreateMapFile(ctx context.Context, name, content string) (string, error)

CreateMapFile creates a new map file using multipart form-data. Returns the reload ID if a reload was triggered (empty string if not) and any error. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) CreateSSLCaFile

func (c *DataplaneClient) CreateSSLCaFile(ctx context.Context, name, content string) (string, error)

CreateSSLCaFile creates a new SSL CA file using multipart form-data. Returns the reload ID if a reload was triggered (empty string if not) and any error. SSL CA file storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) CreateSSLCertificate

func (c *DataplaneClient) CreateSSLCertificate(ctx context.Context, name, content string) (string, error)

CreateSSLCertificate creates a new SSL certificate using multipart form-data, always sending skip_reload=true. Symmetric with UpdateSSLCertificate's existing skip_reload plumbing (added in 77c760c2 "skip_reload=true on aux-file UPDATEs"); the asymmetry on CREATE was an oversight, since the DPAPI's POST /storage/ssl_certificates endpoint declares skip_reload as a query parameter (unlike POST /storage/maps and /storage/general, where the spec doesn't expose it).

Without this, every cert CREATE during PhasePreConfig triggers a DPAPI auto-reload that validates the CURRENT haproxy.cfg against the new on-disk cert. The reload normally succeeds (the new cert is just-written real content), but a parallel reconciliation cycle landing between the cert CREATE's reload and the orchestrator's PhaseConfig push can race against stale-cfg state — same shape as the UPDATE bug the May fix closed.

Returns the reload ID if a reload was triggered (always empty under skip_reload=true) and any error. The name parameter can use dots (e.g., "example.com.pem"), which will be sanitized automatically before calling the API. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) DeleteCRTListFile

func (c *DataplaneClient) DeleteCRTListFile(ctx context.Context, name string) error

DeleteCRTListFile deletes a crt-list file by name, always sending skip_reload=true — without it, the DPAPI schedules its own uncoordinated reload (see DeleteSSLCertificate for the full rationale; deletion only runs for files the live config no longer references, so no reload is needed to apply it). The name parameter can use dots (e.g., "example.com.crtlist"), which will be sanitized automatically before calling the API. CRT-list storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) DeleteGeneralFile

func (c *DataplaneClient) DeleteGeneralFile(ctx context.Context, path string) error

DeleteGeneralFile deletes a general file by path. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) DeleteMapFile

func (c *DataplaneClient) DeleteMapFile(ctx context.Context, name string) error

DeleteMapFile deletes a map file by name. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) DeleteSSLCaFile

func (c *DataplaneClient) DeleteSSLCaFile(ctx context.Context, name string) error

DeleteSSLCaFile deletes an SSL CA file by name. SSL CA file storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) DeleteSSLCertificate

func (c *DataplaneClient) DeleteSSLCertificate(ctx context.Context, name string) error

DeleteSSLCertificate deletes an SSL certificate by name, always sending skip_reload=true. This closes the last gap in the skip_reload family (UPDATE got it in 77c760c2, CREATE followed): without it, the DPAPI answers 202 and its reload agent schedules a SECOND, uncoordinated reload shortly after the deploy's own force_reload config push (deletes run post-config, so the deleted cert is already unreferenced by the live config and running workers keep their in-memory copy — nothing needs a reload). That stray reload blacks out the master CLI socket mid-rollout, which is exactly the window issue #67 captured: endpoint fast-path `set server` pushes fail against the re-executing master while the outgoing worker drains with a stale server list, turning a routine single-replica rollout into a 503.

The name parameter can use dots (e.g., "example.com.pem"), which will be sanitized automatically before calling the API. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) DetectedVersion

func (c *DataplaneClient) DetectedVersion() string

DetectedVersion returns the full version string of the DataPlane API server.

func (*DataplaneClient) Dispatch

func (c *DataplaneClient) Dispatch(ctx context.Context, call CallFunc[*http.Response]) (*http.Response, error)

Dispatch executes the appropriate versioned function based on the detected API version. This is the primary method for executing API calls that work across all versions.

Returns error if:

  • The client type is unexpected
  • The version-specific function is nil
  • The versioned function itself returns an error

Example:

resp, err := c.Dispatch(ctx, CallFunc[*http.Response]{
    V32: func(c *v32.Client) (*http.Response, error) {
        return c.GetAllStorageMapFiles(ctx)
    },
    V31: func(c *v31.Client) (*http.Response, error) {
        return c.GetAllStorageMapFiles(ctx)
    },
    V30: func(c *v30.Client) (*http.Response, error) {
        return c.GetAllStorageMapFiles(ctx)
    },
})

func (*DataplaneClient) DispatchWithCapability

func (c *DataplaneClient) DispatchWithCapability(
	ctx context.Context,
	call CallFunc[*http.Response],
	capabilityCheck func(Capabilities) error,
) (*http.Response, error)

DispatchWithCapability executes the appropriate versioned function with an optional capability check. Use this for version-specific features (e.g., crt-list only available in v3.2+).

The capability check is performed before executing the versioned function. If the check fails, the function is not executed and the capability error is returned.

Parameters:

  • ctx: Context for the API call
  • call: Version-specific functions to execute
  • capabilityCheck: Optional function to verify feature availability. If nil, no check is performed.

Returns error if:

  • Capability check fails
  • The client type is unexpected
  • The version-specific function is nil
  • The versioned function itself returns an error

Example (CRT-list only in v3.2+):

resp, err := c.DispatchWithCapability(ctx, CallFunc[*http.Response]{
    V32: func(c *v32.Client) (*http.Response, error) {
        return c.GetAllStorageSSLCrtListFiles(ctx)
    },
    // V31 and V30 omitted - not supported
}, func(caps Capabilities) error {
    if !caps.SupportsCrtList {
        return errors.New("crt-list storage requires DataPlane API v3.2+")
    }
    return nil
})

func (*DataplaneClient) GetAllCRTListFiles

func (c *DataplaneClient) GetAllCRTListFiles(ctx context.Context) ([]string, error)

GetAllCRTListFiles retrieves all crt-list file names from the storage. Note: This returns only crt-list file names, not the file contents. Use GetCRTListFileContent to retrieve the actual file contents. CRT-list storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) GetAllGeneralFiles

func (c *DataplaneClient) GetAllGeneralFiles(ctx context.Context) ([]string, error)

GetAllGeneralFiles retrieves all general file paths from the storage. Note: This returns only file paths, not the file contents. Use GetGeneralFileContent to retrieve the actual file contents. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) GetAllMapFiles

func (c *DataplaneClient) GetAllMapFiles(ctx context.Context) ([]string, error)

GetAllMapFiles retrieves all map file names from the storage. Note: This returns only map file names, not the file contents. Use GetMapFileContent to retrieve the actual file contents. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) GetAllSSLCaFiles

func (c *DataplaneClient) GetAllSSLCaFiles(ctx context.Context) ([]string, error)

GetAllSSLCaFiles retrieves all SSL CA file names from the runtime storage. Note: This returns only CA file names, not the file contents. Use GetSSLCaFileContent to retrieve the actual file contents. SSL CA file storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) GetAllSSLCertificates

func (c *DataplaneClient) GetAllSSLCertificates(ctx context.Context) ([]string, error)

GetAllSSLCertificates retrieves all SSL certificate names from the storage. Note: This returns only certificate names, not the certificate contents. Use GetSSLCertificateContent to retrieve the actual certificate contents. The returned names are unsanitized (dots restored) for user convenience. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) GetCRTListFileContent

func (c *DataplaneClient) GetCRTListFileContent(ctx context.Context, name string) (string, error)

GetCRTListFileContent retrieves the content of a specific crt-list file by name. The name parameter can use dots (e.g., "example.com.crtlist"), which will be sanitized automatically before calling the API. CRT-list storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) GetGeneralFileContent

func (c *DataplaneClient) GetGeneralFileContent(ctx context.Context, path string) (string, error)

GetGeneralFileContent retrieves the content of a specific general file by path. The API returns the raw file content as application/octet-stream. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) GetMapFileContent

func (c *DataplaneClient) GetMapFileContent(ctx context.Context, name string) (string, error)

GetMapFileContent retrieves the content of a specific map file by name. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) GetRawConfiguration

func (c *DataplaneClient) GetRawConfiguration(ctx context.Context) (string, error)

GetRawConfiguration retrieves the current HAProxy configuration as a string.

This fetches the raw configuration file content from the Dataplane API. The configuration can be parsed using the parser package to get structured data. Works with all HAProxy DataPlane API versions (v3.0+).

Example:

config, err := dpClient.GetRawConfiguration(context.Background())
if err != nil {
    slog.Error("Failed to get config", "error", err)
    os.Exit(1)
}
fmt.Printf("Current config:\n%s\n", config)

func (*DataplaneClient) GetReloadStatus

func (c *DataplaneClient) GetReloadStatus(ctx context.Context, reloadID string) (*ReloadInfo, error)

GetReloadStatus retrieves the status of a specific HAProxy reload operation.

This method polls the DataPlane API to check if an async reload has completed. Use this after receiving a 202 response from configuration changes to verify the reload succeeded. Works with all HAProxy DataPlane API versions (v3.0+).

Parameters:

  • reloadID: The reload identifier from the Reload-ID header

Returns:

  • ReloadInfo: Current status and details of the reload
  • error: Error if the API call fails or reload ID not found

Example:

info, err := dpClient.GetReloadStatus(ctx, "abc123")
if err != nil {
    log.Fatal(err)
}
switch info.Status {
case ReloadStatusSucceeded:
    log.Println("Reload completed successfully")
case ReloadStatusFailed:
    log.Printf("Reload failed: %s", info.Response)
case ReloadStatusInProgress:
    log.Println("Reload still in progress")
}

func (*DataplaneClient) GetRuntimeMapEntries

func (c *DataplaneClient) GetRuntimeMapEntries(ctx context.Context, name string) (map[string]string, error)

GetRuntimeMapEntries returns the live (in-memory) entries of a runtime map as a key→value map (last value wins on duplicate keys). Exposed for tests that need to assert the worker's in-memory map state, not just the on-disk file.

func (*DataplaneClient) GetSSLCaFileContent

func (c *DataplaneClient) GetSSLCaFileContent(ctx context.Context, name string) (string, error)

GetSSLCaFileContent retrieves the content of a specific SSL CA file by name. SSL CA file storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) GetSSLCertificateContent

func (c *DataplaneClient) GetSSLCertificateContent(ctx context.Context, name string) (string, error)

GetSSLCertificateContent retrieves the SHA256 fingerprint for a specific SSL certificate by name.

This function returns the sha256_finger_print field from the HAProxy Data Plane API, which serves as a unique identifier for the certificate content. This allows content-based comparison without needing to download the actual PEM data.

The API provides rich metadata including:

  • sha256_finger_print: SHA-256 hash of certificate content (returned by this function)
  • serial: Certificate serial number
  • issuers: Certificate issuer information
  • subject: Certificate subject information
  • not_after, not_before: Certificate validity period

The name parameter can use dots (e.g., "example.com.pem"), which will be sanitized automatically before calling the API.

Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) GetVersion

func (c *DataplaneClient) GetVersion(ctx context.Context) (int64, error)

GetVersion retrieves the current configuration version from the Dataplane API.

The version is used for optimistic locking when making configuration changes. This prevents concurrent modifications from conflicting. Works with all HAProxy DataPlane API versions (v3.0+).

Example:

version, err := dpClient.GetVersion(context.Background())
if err != nil {
    slog.Error("Failed to get version", "error", err)
    os.Exit(1)
}
fmt.Printf("Current version: %d\n", version)

func (*DataplaneClient) PreferredClient

func (c *DataplaneClient) PreferredClient() any

PreferredClient returns the most appropriate versioned client based on the detected server version. Returns one of: *v30.Client, *v31.Client, *v32.Client.

For most operations, you should use the wrapper methods instead of this. This is provided for operations that aren't wrapped yet.

func (*DataplaneClient) PushRawConfiguration

func (c *DataplaneClient) PushRawConfiguration(ctx context.Context, config string, version int64) (string, error)

PushRawConfiguration pushes a new HAProxy configuration to the Dataplane API.

This triggers a full HAProxy reload and is the production apply path for structural changes (anything outside the runtime-eligible server-field set). When every change is runtime-eligible, prefer PushRawConfigurationSkipReload, which writes the new config to disk and applies the server changes to the running worker without a reload. Works with all HAProxy DataPlane API versions (v3.0+).

Parameters:

  • config: The complete HAProxy configuration string
  • version: The expected configuration version for optimistic locking. The version is incremented after a successful push.

Returns:

  • reloadID: The reload identifier from the Reload-ID header (if reload triggered)
  • error: Error if the push fails

Example:

reloadID, err := dpClient.PushRawConfiguration(context.Background(), newConfig, 1)
if err != nil {
    slog.Error("Failed to push config", "error", err)
    os.Exit(1)
}
if reloadID != "" {
    slog.Info("HAProxy reloaded", "reload_id", reloadID)
}

func (*DataplaneClient) PushRawConfigurationSkipReload

func (c *DataplaneClient) PushRawConfigurationSkipReload(ctx context.Context, config string, version int64, runtimeActions string) error

PushRawConfigurationSkipReload pushes the full config to disk without triggering a reload. The runtimeActions string is a semicolon-separated list of runtime socket commands that HAProxy applies immediately via stats socket after the config file is written. (e.g., "SetServerAddr backend srv 10.0.0.1 8080;SetServerState backend srv ready"). This allows N server state changes to be applied atomically in a single API call, replacing N serial ReplaceServerBackend calls that each re-read haproxy.cfg from disk.

func (*DataplaneClient) PushRawConfigurationSkipReloadSkipVersion

func (c *DataplaneClient) PushRawConfigurationSkipReloadSkipVersion(ctx context.Context, config, runtimeActions string) error

PushRawConfigurationSkipReloadSkipVersion is the queue-bypass variant of PushRawConfigurationSkipReload: it applies the runtime actions without a reload AND without the optimistic-locking version check (skip_version).

The deployer's runtime bypass fires this immediately when a queued reconcile produces runtime-eligible server changes, OUTSIDE the deployment scheduler's serialization — so a pod-IP rotation reaches the live worker in ~ms instead of waiting in the pending slot behind an in-flight ~200ms structural reload. The caller passes the CURRENT on-disk config as the body (so a co-batched reconcile's structural changes are NOT written to disk without a reload); only the runtime actions take effect on the live worker.

NB: skip_version does NOT bump the config version — the dataplane writes the pushed body VERBATIM, without the `# _version=N` header and without incrementing anything (client-native raw.go writes the header only on the versioned path; transaction.go skips IncrementTransactionVersion when skipVersion is set). After this push GetVersion reads the missing header as 1, the headerless sentinel. The orchestrator therefore treats version 1 as uncacheable and never satisfies a version-cache check with it (see headerlessConfigVersion in pkg/dataplane), so the next versioned sync always re-fetches the pod's actual config and re-stamps the header. The runtime change persists across the scheduled deploy's structural reload because that deploy re-renders the current endpoints (config-driven; no server-state-file — ADR-0011).

func (*DataplaneClient) ReplaceRuntimeMap

func (c *DataplaneClient) ReplaceRuntimeMap(ctx context.Context, name, desiredContent string) error

ReplaceRuntimeMap makes the live (in-memory) contents of an existing runtime map equal the entries parsed from desiredContent, by applying the minimal per-entry delta (add/delete) against the current runtime state. It applies WITHOUT a reload and is available on all DataPlane API versions (v3.0+).

It deliberately does NOT use a bulk "add payload" (which appends rather than replaces, leaving stale and duplicate entries) nor a clear+repopulate (which would briefly empty the whole map). A changed single-value key is updated in-place with `set map` (atomic, no gap); only multi-value keys and removals use del(+add). Unchanged keys are untouched, so the map is always valid.

force_sync is intentionally NOT set: the orchestrator's pre-config phase already wrote the desired content to the on-disk map file (skip_reload), so disk durability (reload convergence) is handled there. This call only updates the live worker's memory, and must not push memory back to disk.

name is the map's identifier as HAProxy reports it (the path used in the config); the map must already exist and be referenced by the running config.

func (*DataplaneClient) ReplaceRuntimeSSLCaFiles

func (c *DataplaneClient) ReplaceRuntimeSSLCaFiles(ctx context.Context, contentByPath map[string]string) error

ReplaceRuntimeSSLCaFiles replaces the live (in-memory) contents of one or more already-loaded SSL CA files (mTLS trust bundles) on the worker via the runtime API (`add ssl ca-file` + `commit ssl ca-file`, which replaces the file with the payload — see AddRuntimeCaFileEntry for why `add` is used instead of `set`), WITHOUT a reload. contentByPath maps each ca-file's config path (the `ca-file <path>` argument HAProxy loaded) to its new bundle. Available on DataPlane API v3.2+ only — callers must gate on Capabilities().SupportsSslCaFiles.

The loaded-ca-file list is fetched ONCE and reused to resolve every file's runtime identifier, so N rotations cost a single list fetch. Like ReplaceRuntimeMap / ReplaceRuntimeSSLCerts, disk durability is left to the orchestrator's pre-config general-storage write (skip_reload); this call only updates the live worker's memory.

This is the controller-side equivalent of what the SPIFFE cert-reloader sidecar does over the raw stats socket (`set ssl ca-file <bundle>`), routed through the DataPlane API instead.

func (*DataplaneClient) ReplaceRuntimeSSLCerts

func (c *DataplaneClient) ReplaceRuntimeSSLCerts(ctx context.Context, pemByName map[string]string) error

ReplaceRuntimeSSLCerts replaces the live (in-memory) contents of one or more already-loaded SSL certificates on the worker via the runtime API (set ssl cert + commit ssl cert, applied atomically per cert by the DataPlane API), WITHOUT a reload. pemByName maps each cert's config identifier to its new PEM. Available on DataPlane API v3.2+ only — callers must gate on Capabilities().SupportsRuntimeSSLCerts; older versions take the reload path.

The loaded-cert list is fetched ONCE and reused to resolve every cert's runtime identifier, so N rotations cost a single list fetch, not N.

Like ReplaceRuntimeMap, disk durability is left to the orchestrator's pre-config storage write (skip_reload); this updates worker memory only and carries no force_sync.

func (*DataplaneClient) UpdateCRTListFile

func (c *DataplaneClient) UpdateCRTListFile(ctx context.Context, name, content string) (string, error)

UpdateCRTListFile updates an existing crt-list file using text/plain content-type, always sending skip_reload=true (see DeleteSSLCertificate for the rationale — the deploy pipeline's config push is the only coordinated reload trigger). Returns the reload ID if a reload was triggered (always empty under skip_reload=true) and any error. Note: The Dataplane API requires text/plain or application/json for UPDATE operations, while CREATE operations accept multipart/form-data. The CREATE endpoint declares no skip_reload parameter at all, which is why production crt-lists are stored as general files instead (see paths.go). The name parameter can use dots (e.g., "example.com.crtlist"), which will be sanitized automatically before calling the API. CRT-list storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) UpdateGeneralFile

func (c *DataplaneClient) UpdateGeneralFile(ctx context.Context, path, content string) (string, error)

UpdateGeneralFile updates an existing general file using multipart form-data. Always sends skip_reload=true so the dataplane API does NOT auto-reload after the PUT. The new content is written to disk; HAProxy keeps using the in-memory copy until the next reload. This matches Create's 201-with-no-reload behavior and lets the orchestrator batch every aux-file change into the single reload that the main config sync triggers (or, when only aux files changed, the explicit force-reload at the end of the config sync).

The previous behavior (default reload after PUT) caused an auxiliary-reload race on route deletion: the new spoe.conf could land before the new haproxy.cfg, and the auto-reload would fire against the stale haproxy.cfg whose `send-spoe-group <name>` references no longer resolved.

Returns the reload ID if a reload was triggered (always empty under skip_reload=true) and any error. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) UpdateMapFile

func (c *DataplaneClient) UpdateMapFile(ctx context.Context, name, content string) (string, error)

UpdateMapFile updates an existing map file using text/plain content-type. Always sends skip_reload=true; see UpdateGeneralFile for the rationale (auto-reload after PUT raced against stale haproxy.cfg on shrinking aux content). Returns the reload ID if a reload was triggered (always empty under skip_reload=true) and any error. Note: The Dataplane API requires text/plain or application/json for UPDATE operations, while CREATE operations accept multipart/form-data. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) UpdateSSLCaFile

func (c *DataplaneClient) UpdateSSLCaFile(ctx context.Context, name, content string) (string, error)

UpdateSSLCaFile updates an existing SSL CA file using multipart form-data. Returns the reload ID if a reload was triggered (empty string if not) and any error. SSL CA file storage is only available in HAProxy DataPlane API v3.2+.

func (*DataplaneClient) UpdateSSLCertificate

func (c *DataplaneClient) UpdateSSLCertificate(ctx context.Context, name, content string) (string, error)

UpdateSSLCertificate updates an existing SSL certificate using text/plain content. Always sends skip_reload=true; see UpdateGeneralFile for the rationale. The new PEM is written to disk but HAProxy keeps serving the old cert in memory until the next reload, which the orchestrator triggers explicitly when only aux files changed (and the main config sync's commit triggers it otherwise). Returns the reload ID if a reload was triggered (always empty under skip_reload=true) and any error. The name parameter can use dots (e.g., "example.com.pem"), which will be sanitized automatically before calling the API. Works with all HAProxy DataPlane API versions (v3.0+).

func (*DataplaneClient) VerifyRuntimeMap

func (c *DataplaneClient) VerifyRuntimeMap(ctx context.Context, name, desiredContent string) (int, error)

VerifyRuntimeMap re-reads the live runtime map and reports how many per-entry mutations would still be needed to make it match desiredContent (0 = converged). The orchestrator's pure-runtime lane calls this right after ReplaceRuntimeMap as a read-back check: runtime map mutations are acknowledged by the Dataplane API even when the underlying master-socket command was lost in flight (observed on the haproxytech 3.1 image under reload churn — issue #48), and without a read-back the divergence latches — the on-disk file already matches desired, so no later deploy re-runs ReplaceRuntimeMap for the map and only an unrelated reload heals it.

type Endpoint

type Endpoint struct {
	URL      string
	Username string
	Password string
	PodName  string // Kubernetes pod name for observability

	// Cached version info (optional, avoids redundant /v3/info calls if set)
	CachedMajorVersion int
	CachedMinorVersion int
	CachedFullVersion  string
	CachedIsEnterprise bool // True if this is HAProxy Enterprise edition
}

Endpoint represents HAProxy Dataplane API connection information. This is a convenience type alias to avoid circular imports.

func (*Endpoint) HasCachedVersion

func (e *Endpoint) HasCachedVersion() bool

HasCachedVersion returns true if version info has been cached.

type ReloadInfo

type ReloadInfo struct {
	// ID is the unique identifier for this reload operation.
	ID string
	// Status is the current status of the reload.
	Status ReloadStatus
	// Response contains error details if the reload failed.
	Response string
	// ReloadTimestamp is the Unix timestamp when the reload occurred.
	ReloadTimestamp int64
}

ReloadInfo contains information about a HAProxy reload operation.

type ReloadStatus

type ReloadStatus string

ReloadStatus represents the status of a HAProxy reload operation.

const (
	// ReloadStatusInProgress indicates the reload is still being processed.
	ReloadStatusInProgress ReloadStatus = "in_progress"
	// ReloadStatusSucceeded indicates the reload completed successfully.
	ReloadStatusSucceeded ReloadStatus = "succeeded"
	// ReloadStatusFailed indicates the reload failed (HAProxy reverted to previous config).
	ReloadStatusFailed ReloadStatus = "failed"
)

type RetryCondition

type RetryCondition func(err error) bool

RetryCondition determines whether an error should trigger a retry.

func IsConnectionError

func IsConnectionError() RetryCondition

IsConnectionError returns a RetryCondition that retries on transient connection errors.

This condition detects network-level connection failures such as:

  • Connection refused (ECONNREFUSED) - server not yet accepting connections
  • Connection reset (ECONNRESET) - connection lost during communication

These errors are typically transient and resolve within a few seconds as services start up or network conditions stabilize.

This condition does NOT retry on:

  • HTTP errors (4xx, 5xx status codes)
  • Authentication failures
  • Parsing errors
  • Context cancellation

Example:

retryConfig := RetryConfig{
    MaxAttempts: 3,
    RetryIf:     IsConnectionError(),
    Backoff:     BackoffExponential,
    BaseDelay:   100 * time.Millisecond,
}
result, err := WithRetry(ctx, retryConfig, func(attempt int) (*Result, error) {
    return fetchFromAPI(ctx)
})

func IsReloadInProgress

func IsReloadInProgress() RetryCondition

IsReloadInProgress returns a RetryCondition that retries a runtime apply that failed because HAProxy was mid-reload. While the dataplaneapi drives a reload its master socket is briefly unavailable, so runtime commands fail transiently with signatures like:

  • "cannot execute SetServerState: ... haproxy-master.sock: connect: connection refused"
  • "runtime server '<be>/<srv>' not found" (the runtime view is reloading)

A reload completes in tens-to-low-hundreds of ms, so a short bounded retry lets the runtime change land right after it — instead of leaving the new slot unset until the next reconcile, which is the rolling-restart gap that produced 503s under parallel-test reload churn. Version conflicts (409) and other genuine 4xx do NOT match these markers, so they fall through to the caller unchanged.

type RetryConfig

type RetryConfig struct {
	// MaxAttempts is the maximum number of attempts (including the first one).
	// Must be >= 1. Default: 1 (no retries).
	MaxAttempts int

	// RetryIf determines whether to retry based on the error.
	// If nil, no retries are performed.
	RetryIf RetryCondition

	// Backoff strategy for delays between retries.
	// Default: zero value (no delay between retries).
	Backoff BackoffStrategy

	// BaseDelay is the initial delay for BackoffExponential (doubles each retry).
	// Default: 100ms
	BaseDelay time.Duration

	// Logger for retry attempts. If nil, no logging is performed.
	Logger *slog.Logger
}

RetryConfig configures retry behavior for operations.

type Version

type Version struct {
	Major int
	Minor int
	Patch int
	Full  string // Original version string, retained for logging
}

Version represents a DataPlane API / HAProxy version (major.minor.patch). Patch is best-effort: it is 0 when the source string carries only "major.minor" or a non-numeric patch segment.

This is the single version type for the project; pkg/dataplane aliases it as dataplane.Version.

func ParseVersion

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

ParseVersion parses a DataPlane API / HAProxy version string into a Version. Examples: "v3.2.6 87ad0bcf" -> {3, 2, 6}, "3.3" -> {3, 3, 0}.

func (*Version) Compare

func (v *Version) Compare(other *Version) int

Compare orders two versions by major, then minor. Patch is INTENTIONALLY ignored: Compare is used for series compatibility between two versions on the SAME axis — e.g. the testrunner gating a test on minHAProxyVersion, comparing two HAProxy binary versions that share major.minor but never patch. Do NOT use it to compare a DataPlane API version against a HAProxy binary version: as of HAProxy 3.4 those decouple at the minor level (the 3.4 image ships DataPlane API v3.3), so only their major versions are comparable. Returns -1 if v < other, 0 if same series, 1 if v > other.

type VersionInfo

type VersionInfo struct {
	API struct {
		Version string `json:"version"` // e.g., "v3.2.6 87ad0bcf"
	} `json:"api"`
}

VersionInfo contains detected version information from /v3/info endpoint.

func DetectVersion

func DetectVersion(ctx context.Context, endpoint *Endpoint, _ *slog.Logger) (*VersionInfo, error)

DetectVersion queries the /v3/info endpoint to identify the DataPlane API version advertised by the given endpoint. Callers should inspect the result with ParseVersion and IsEnterpriseVersion before deriving Capabilities.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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