debug

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

README

pkg/controller/debug

Controller-specific introspection.Var implementations that expose controller state on the /debug/vars endpoint. This package is the bridge between the generic pkg/introspection server and the controller's internal state cache — operators querying /debug/vars/config, /debug/vars/rendered, etc. are hitting the Vars defined here.

How It Fits Together

Controller (pkg/controller)
    │
    ├── StateCache — caches config / credentials / rendered / aux / resources
    │   from events, implements StateProvider
    │
    └── starts introspection.Server ──> Registry ──┐
                                                   │
                                      ┌────────────┴───────────┐
                                      │   debug.RegisterVariables
                                      │   (registers each Var)
                                      ▼
                       ConfigVar, CredentialsVar, RenderedVar,
                       AuxFilesVar, ResourcesVar, FullStateVar,
                       PipelineVar, ValidatedVar, ErrorsVar, EventsVar

Each Var has one responsibility: fetch a piece of state from the StateProvider (or the event buffer), shape it into JSON, and return it when the introspection server calls .Get().

StateProvider

The controller supplies a StateProvider that exposes cached state under an RWMutex:

type StateProvider interface {
    GetConfig() (*config.Config, string, error)               // cfg, version, err
    GetCredentials() (*config.Credentials, string, error)     // creds metadata only
    GetRenderedConfig() (string, time.Time, error)
    GetAuxiliaryFiles() (*dataplane.AuxiliaryFiles, time.Time, error)
    GetResourceCounts() (map[string]int, error)
    GetResourcesByType(resourceType string) ([]any, error)
    // plus pipeline / validation / error accessors used by
    // PipelineVar, ValidatedVar, ErrorsVar
}

Every method returns a "not ready" error until the relevant event has been observed — callers get a clean 404-ish response instead of empty payloads during startup.

Registering

import (
    "context"

    "gitlab.com/haproxy-haptic/haptic/pkg/controller/debug"
    "gitlab.com/haproxy-haptic/haptic/pkg/introspection"
)

registry := introspection.NewRegistry()

eventBuffer := debug.NewEventBuffer(1000, eventBus)
go eventBuffer.Start(ctx)

debug.RegisterVariables(registry, stateProvider, eventBuffer)

server := introspection.NewServer(":8080", registry)
go server.Start(ctx)

EventBuffer subscribes to the EventBus separately from the commentator so that the debug endpoint has an independent ring-buffered history for /debug/vars/events — the two consumers can be tuned and cleaned up in isolation.

Exposed Paths

Each of these maps to a Var in this package:

Path Var Returns
/debug/vars/config ConfigVar Parsed config + version + load timestamp
/debug/vars/credentials CredentialsVar Metadata only (has_dataplane_creds, version) — never the passwords
/debug/vars/rendered RenderedVar Last rendered haproxy.cfg + size + timestamp
/debug/vars/auxfiles AuxFilesVar Last rendered maps / SSL / general / crt-list files + counts
/debug/vars/resources ResourcesVar Per-type resource counts
/debug/vars/events EventsVar Ring-buffered event history (last N)
/debug/vars/state FullStateVar Aggregate of the above — large; prefer the specific paths
/debug/vars/pipeline PipelineVar Last pipeline execution metadata (stages, timings)
/debug/vars/validated ValidatedVar Last validation result (syntax + semantic phases)
/debug/vars/errors ErrorsVar Recent per-component errors
/debug/vars/uptime introspection.Func (computed on demand, not a struct) Process uptime: started timestamp + uptime_seconds + uptime_string

The introspection server supports ?field={...} JSONPath on every response, so operators can narrow to a specific field without downloading the whole payload.

pkg/controller/debug also registers a non-Var endpoint via RegisterEventsHandler: /debug/events (note: no /vars/ segment) supports ?limit=N and ?correlation_id=<id> for richer event queries than the ring-buffered /debug/vars/events Var.

Security

  • CredentialsVar is deliberately built to expose metadata only. It constructs its response map field-by-field — no marshalling of the Credentials struct, so a future accidental field addition can't leak the password by default.
  • FullStateVar includes the rendered haproxy.cfg, which references internal hostnames and backend IPs. Operators should restrict the debug port with a NetworkPolicy (see docs/controller/docs/operations/security.md).
  • Validators in this package never read raw Credentials.Password fields for their own responses — the StateProvider.GetCredentials signature returns *config.Credentials but the CredentialsVar.Get() implementation picks out individual safe fields.

Testing

go test ./pkg/controller/debug/...            # unit tests
go test ./pkg/controller/debug/... -race      # race detector

Each Var has a test that constructs a mock StateProvider, calls .Get(), and asserts on the returned shape — including explicit "no password in output" assertions on CredentialsVar to prevent regressions.

See Also

  • pkg/introspection — generic registry + HTTP server this package plugs into
  • pkg/controller/debug/CLAUDE.md — developer context, walkthrough for adding a new Var
  • docs/controller/docs/operations/debugging.md — operator-facing view of the same endpoints
  • tests/acceptance/debug_client.go — end-to-end consumer that polls these endpoints

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package debug provides controller-specific debug variable implementations.

This package integrates the generic pkg/introspection infrastructure with controller-specific state. It defines:

  • StateProvider interface for accessing controller internal state
  • Var implementations (ConfigVar, RenderedVar, etc.)
  • Event buffer for tracking recent events
  • Registration logic for publishing variables

The controller implements StateProvider and provides thread-safe access to its internal state (config, rendered output, resources, etc.).

Index

Constants

View Source
const ComponentName = "event-buffer"

ComponentName is the unique identifier for the event buffer component.

Variables

This section is empty.

Functions

func RegisterEventsHandler

func RegisterEventsHandler(server *introspection.Server, eventBuffer *EventBuffer)

RegisterEventsHandler registers the /debug/events endpoint with correlation query support.

This handler supports:

  • GET /debug/events - Returns last 100 events
  • GET /debug/events?limit=N - Returns last N events
  • GET /debug/events?correlation_id=<id> - Returns events with matching correlation ID

Example:

debug.RegisterEventsHandler(server, eventBuffer)
go server.Start(ctx)

func RegisterVariables

func RegisterVariables(
	registry *introspection.Registry,
	provider StateProvider,
	eventBuffer *EventBuffer,
)

RegisterVariables registers all controller debug variables with the registry.

This function should be called during controller initialization, after components are set up but before the debug server starts.

Registered variables:

  • config: Current controller configuration
  • credentials: Credential metadata (not actual values)
  • rendered: Last rendered HAProxy config
  • auxfiles: Auxiliary files (SSL, maps, etc.)
  • resources: Resource counts by type
  • events: Recent events (default: last 100)
  • state: Full state dump (use carefully - large response)
  • uptime: Time since controller started
  • pipeline: Reconciliation pipeline status (trigger, render, validate, deploy)
  • validated: Last successfully validated HAProxy config
  • errors: Aggregated error summary across all phases

Example:

registry := introspection.NewRegistry()
eventBuffer := debug.NewEventBuffer(1000, bus)
debug.RegisterVariables(registry, controller, eventBuffer)

server := introspection.NewServer(":6060", registry)
go server.Start(ctx)

Types

type DeploymentStatus

type DeploymentStatus struct {
	Status             string           `json:"status"` // "succeeded" | "failed" | "skipped" | "pending"
	Reason             string           `json:"reason,omitempty"`
	Timestamp          time.Time        `json:"timestamp"`
	DurationMs         int64            `json:"duration_ms,omitempty"`
	EndpointsTotal     int              `json:"endpoints_total"`
	EndpointsSucceeded int              `json:"endpoints_succeeded"`
	EndpointsFailed    int              `json:"endpoints_failed"`
	FailedEndpoints    []FailedEndpoint `json:"failed_endpoints,omitempty"`
}

DeploymentStatus represents the deployment phase status.

type ErrorInfo

type ErrorInfo struct {
	Timestamp time.Time `json:"timestamp"`
	Errors    []string  `json:"errors"`
}

ErrorInfo contains details about a specific error.

type ErrorSummary

type ErrorSummary struct {
	ConfigParseError       *ErrorInfo  `json:"config_parse_error,omitempty"`
	TemplateRenderError    *ErrorInfo  `json:"template_render_error,omitempty"`
	HAProxyValidationError *ErrorInfo  `json:"haproxy_validation_error,omitempty"`
	DeploymentErrors       []ErrorInfo `json:"deployment_errors,omitempty"`
	LastErrorTimestamp     time.Time   `json:"last_error_timestamp"`
}

ErrorSummary provides an aggregated view of recent errors across all phases.

type Event

type Event struct {
	Timestamp     time.Time `json:"timestamp"`
	Type          string    `json:"type"`
	Summary       string    `json:"summary"`
	Details       any       `json:"details,omitempty"`
	CorrelationID string    `json:"correlation_id,omitempty"`
}

Event represents a debug event with timestamp and details.

This is a simplified representation of controller events for debug purposes. It captures the essential information without exposing internal event structures.

type EventBuffer

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

EventBuffer maintains a ring buffer of recent events for debug purposes.

This is separate from the EventCommentator's ring buffer to avoid coupling debug functionality to the commentator component. It subscribes to the EventBus and stores simplified event representations.

func NewEventBuffer

func NewEventBuffer(size int, eventBus *busevents.EventBus) *EventBuffer

NewEventBuffer creates a new event buffer with the specified capacity.

The buffer subscribes to all events from the EventBus and stores the last N events (where N is the size parameter).

Note: The buffer subscribes during construction per CLAUDE.md guidelines to ensure subscription happens before EventBus.Start() is called.

Example:

eventBuffer := debug.NewEventBuffer(1000, eventBus)
go eventBuffer.Start(ctx)

func (*EventBuffer) FindByCorrelationID

func (eb *EventBuffer) FindByCorrelationID(correlationID string) []Event

FindByCorrelationID returns events matching the specified correlation ID.

This method searches through all events in the buffer and returns those that have a matching correlation ID. Events are returned in chronological order.

Example:

events := eventBuffer.FindByCorrelationID("550e8400-e29b-41d4-a716-446655440000")

func (*EventBuffer) GetLast

func (eb *EventBuffer) GetLast(n int) []Event

GetLast returns the last n events in chronological order.

Example:

recent := eventBuffer.GetLast(100)  // Last 100 events

func (*EventBuffer) Start

func (eb *EventBuffer) Start(ctx context.Context) error

Start begins collecting events from the EventBus.

This method blocks until the context is cancelled. It processes events from the pre-subscribed channel. It should be run in a goroutine.

Example:

go eventBuffer.Start(ctx)

type EventsVar

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

EventsVar exposes recent events as a debug variable.

Returns a JSON array of recent events.

Example response:

[
  {
    "timestamp": "2025-01-15T10:30:45Z",
    "type": "config.validated",
    "summary": "config.validated"
  },
  {
    "timestamp": "2025-01-15T10:30:46Z",
    "type": "reconciliation.triggered",
    "summary": "reconciliation.triggered"
  }
]

func (*EventsVar) Get

func (v *EventsVar) Get() (any, error)

Get implements introspection.Var.

type FailedEndpoint

type FailedEndpoint struct {
	URL   string `json:"url"`
	Error string `json:"error"`
}

FailedEndpoint contains details about a failed deployment endpoint.

type PipelineStatus

type PipelineStatus struct {
	LastTrigger *TriggerStatus    `json:"last_trigger"`
	Rendering   *RenderingStatus  `json:"rendering"`
	Validation  *ValidationStatus `json:"validation"`
	Deployment  *DeploymentStatus `json:"deployment"`
}

PipelineStatus represents the complete status of the last reconciliation pipeline.

This provides visibility into each stage of the pipeline: trigger, rendering, validation, and deployment.

type RenderingStatus

type RenderingStatus struct {
	Status      string    `json:"status"` // "succeeded" | "failed"
	Timestamp   time.Time `json:"timestamp"`
	DurationMs  int64     `json:"duration_ms"`
	ConfigBytes int       `json:"config_bytes"`
	Error       string    `json:"error,omitempty"`
}

RenderingStatus represents the template rendering phase status.

type StateProvider

type StateProvider interface {
	// GetConfig returns the current validated configuration and its version.
	//
	// Returns error if config is not yet loaded.
	//
	// Example return:
	//   config: &config.Config{...}
	//   version: "v123"
	//   error: nil
	GetConfig() (*config.Config, string, error)

	// GetCredentials returns the current credentials and their version.
	//
	// Returns error if credentials are not yet loaded.
	//
	// Example return:
	//   creds: &config.Credentials{...}
	//   version: "v456"
	//   error: nil
	GetCredentials() (*config.Credentials, string, error)

	// GetRenderedConfig returns the most recently rendered HAProxy configuration
	// and the timestamp when it was rendered.
	//
	// Returns error if no config has been rendered yet.
	//
	// Example return:
	//   rendered: "global\n  maxconn 2000\n..."
	//   timestamp: 2025-01-15 10:30:45
	//   error: nil
	GetRenderedConfig() (string, time.Time, error)

	// GetAuxiliaryFiles returns the most recently used auxiliary files
	// (SSL certificates, map files, general files) and the timestamp.
	//
	// Returns error if no auxiliary files have been cached yet.
	//
	// Example return:
	//   auxFiles: &dataplane.AuxiliaryFiles{SSLCertificates: [...], ...}
	//   timestamp: 2025-01-15 10:30:45
	//   error: nil
	GetAuxiliaryFiles() (*dataplane.AuxiliaryFiles, time.Time, error)

	// GetResourceCounts returns a map of resource type → count.
	//
	// The keys are resource names as defined in the controller configuration
	// (e.g., "ingresses", "services", "haproxy-pods").
	//
	// Example return:
	//   {
	//     "ingresses": 5,
	//     "services": 12,
	//     "haproxy-pods": 2
	//   }
	GetResourceCounts() (map[string]int, error)

	// GetResourcesByType returns the resources of a specific type.
	//
	// The resourceType parameter should match a key from GetResourceCounts().
	//
	// Partial-result contract: for `store: on-demand` resource types, the
	// returned slice is the subset currently warm in the store's cache only —
	// NOT the full set of tracked references. Listing the full set would fan
	// out one kube-apiserver lookup per reference, which this method
	// deliberately avoids. The count from GetResourceCounts() is therefore the
	// authoritative total; this slice may be shorter. Callers wiring this to a
	// user-facing endpoint must surface that the result is the warm subset so
	// consumers don't mistake it for the complete set. For `store: full`
	// resource types the result is complete.
	//
	// Returns error if the resource type is not found.
	//
	// Example:
	//   resources, err := provider.GetResourcesByType("ingresses")
	GetResourcesByType(resourceType string) ([]any, error)

	// GetPipelineStatus returns the complete status of the last reconciliation pipeline.
	//
	// Returns error if no pipeline has run yet.
	//
	// Example return:
	//   status: &PipelineStatus{
	//     LastTrigger: &TriggerStatus{Timestamp: ..., Reason: "config_change"},
	//     Rendering:   &RenderingStatus{Status: "succeeded", ...},
	//     Validation:  &ValidationStatus{Status: "failed", Errors: [...]},
	//     Deployment:  &DeploymentStatus{Status: "skipped", Reason: "validation_failed"},
	//   }
	GetPipelineStatus() (*PipelineStatus, error)

	// GetValidatedConfig returns the last successfully validated HAProxy configuration.
	//
	// Unlike GetRenderedConfig(), this only returns configs that passed validation.
	// Returns error if no config has been validated successfully yet.
	//
	// Example return:
	//   info: &ValidatedConfigInfo{
	//     Config:               "global\n  maxconn 2000\n...",
	//     Timestamp:            2025-01-15 10:30:45,
	//     ConfigBytes:          4567,
	//     ValidationDurationMs: 200,
	//   }
	GetValidatedConfig() (*ValidatedConfigInfo, error)

	// GetErrors returns an aggregated summary of recent errors across all phases.
	//
	// This is useful for quick diagnosis of configuration issues.
	//
	// Example return:
	//   summary: &ErrorSummary{
	//     HAProxyValidationError: &ErrorInfo{
	//       Timestamp: 2025-01-15 10:30:47,
	//       Errors:    ["[ALERT] parsing [haproxy.cfg:3] : unknown keyword..."],
	//     },
	//     LastErrorTimestamp: 2025-01-15 10:30:47,
	//   }
	GetErrors() (*ErrorSummary, error)
}

StateProvider provides access to controller internal state.

This interface is implemented by the controller to expose its state to debug variables in a thread-safe manner. The controller caches state by subscribing to events (ConfigValidatedEvent, RenderCompletedEvent, etc.) and updates the cached values accordingly.

All methods must be thread-safe as they may be called concurrently from HTTP request handlers.

type TriggerStatus

type TriggerStatus struct {
	Timestamp time.Time `json:"timestamp"`
	Reason    string    `json:"reason"`
}

TriggerStatus represents what triggered the last reconciliation.

type ValidatedConfigInfo

type ValidatedConfigInfo struct {
	Config               string    `json:"config"`
	Timestamp            time.Time `json:"timestamp"`
	ConfigBytes          int       `json:"config_bytes"`
	ValidationDurationMs int64     `json:"validation_duration_ms"`
}

ValidatedConfigInfo contains information about the last successfully validated config.

type ValidationStatus

type ValidationStatus struct {
	Status     string    `json:"status"` // "succeeded" | "failed" | "pending"
	Timestamp  time.Time `json:"timestamp"`
	DurationMs int64     `json:"duration_ms"`
	Errors     []string  `json:"errors,omitempty"`
	Warnings   []string  `json:"warnings,omitempty"`
}

ValidationStatus represents the HAProxy validation phase status.

Jump to

Keyboard shortcuts

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