conversion

package
v0.6.0 Latest Latest
Warning

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

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

README

pkg/controller/conversion

Converts the HAProxyTemplateConfig CRD (Kubernetes API surface) into the internal *config.Config (controller-internal) the rest of the controller works with.

Overview

Two entry points:

Function Input Output
ParseCRD(resource) *unstructured.Unstructured from a watcher (*config.Config, *v1alpha1.HAProxyTemplateConfig, error) — both the converted internal config and the typed CRD wrapper for k8s metadata
ConvertSpec(spec) typed *v1alpha1.HAProxyTemplateConfigSpec (*config.Config, error) — useful when callers already have the typed object

ParseCRD is the production path; the controller's CRD watcher hands raw unstructured bytes to it, then publishes the resulting *config.Config plus the CRD wrapper as a ConfigParsedEvent. The wrapper is kept around so downstream components (status updater, config publisher) have access to metadata.name / metadata.namespace / metadata.uid for owner references and status patches.

Quick Start

import "github.com/haproxy-haptic/HAPTIC/pkg/controller/conversion"

cfg, crd, err := conversion.ParseCRD(unstructuredResource)
if err != nil {
    // type mismatch, missing fields, or invalid YAML inside spec
    return err
}
// cfg  → *config.Config (templates, watched resources, dataplane settings, etc.)
// crd  → *v1alpha1.HAProxyTemplateConfig (typed CRD with metadata)

Design

The conversion is intentionally a one-way street: CRD types live in pkg/apis/haproxytemplate/v1alpha1 and are generated from kubebuilder annotations; internal *config.Config lives in pkg/core/config and is plain Go. Keeping them separate lets the internal type evolve independently of the CRD schema (and lets tests construct configs without going through YAML).

Validation of field-level constraints (template syntax, JSONPath expressions, port ranges) does not happen here — pkg/controller/configchange.ConfigChangeHandler runs the scatter-gather validation against the converted *config.Config. This package is just the structural conversion.

See Also

License

Apache-2.0 — see root LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssembleSources

func AssembleSources(documents []*unstructured.Unstructured) ([]*unstructured.Unstructured, error)

AssembleSources orders a flat set of documents into merge order: the snippets the config references, in the order it declares them, followed by the config itself so its inline content wins.

Document order is deliberately NOT used. Helm sorts rendered manifests by kind, so a `helm template` stream lists every HAProxyTemplateConfig before any HAProxyTemplateLibrary — the reverse of merge order. spec.libraryRefs is the only ordering authority.

func CompositeVersion

func CompositeVersion(sources []*unstructured.Unstructured) string

CompositeVersion identifies the state of a whole merged set as one string.

The merged object carries the LAST source's metadata, so that object's version alone would not change when only a library changed — and the redundant-reinit guard compares versions for equality, so such a change would be silently filtered out. Naming every member makes the version change whenever any member's SPEC does.

Keyed on metadata.generation, NOT resourceVersion. resourceVersion moves on every write, including ones that change no configuration: the controller's own ownerReference stamping on each library, and its status writes. Each of those then looked like a config change and triggered a full validationTests run — under load the live gate timed out, the change was rejected, and template status patches (Ingress status among them) never applied.

Generation is the right key because the apiserver bumps it on spec changes only: an operator's in-place edit to a library still reinitialises, while a metadata-only patch does not.

func ConfigOf

func ConfigOf(sources []*unstructured.Unstructured) *unstructured.Unstructured

ConfigOf returns the HAProxyTemplateConfig among a merged set, or nil.

Selected by KIND, not by position. Assembly appends the config after the libraries it references, but nothing enforces that, and a reorder would otherwise make callers silently act on a library — stamping status onto a library's identity, or treating the config as one of its own dependencies.

func ConvertSpec

func ConvertSpec(spec *v1alpha1.HAProxyTemplateConfigSpec) (*config.Config, error)

ConvertSpec converts a HAProxyTemplateConfig CRD Spec to internal config.Config format.

This is a comprehensive converter that handles ALL fields from the CRD spec including:

  • Production fields: PodSelector, Controller, Logging, Dataplane
  • Template fields: HAProxyConfig, TemplateSnippets, Maps, Files, SSLCertificates
  • Resource fields: WatchedResources, WatchedResourcesIgnoreFields
  • Configuration fields: TemplatingSettings
  • Test fields: ValidationTests (includes fixtures and assertions)

The CRD spec field CredentialsSecretRef is intentionally excluded as it's handled separately by the credentials loader component.

IMPORTANT: When adding or modifying fields in the CRD types (pkg/apis/haproxytemplate/v1alpha1/types.go), you MUST update this function to copy those fields. The CRD types have documentation comments pointing to this file as a reminder.

Common mistake: Adding a field to the CRD but forgetting to copy it here, resulting in the field being silently ignored by the controller.

func LibrariesOf

func LibrariesOf(sources []*unstructured.Unstructured) []*unstructured.Unstructured

LibrariesOf returns the HAProxyTemplateLibrary sources, in merge order.

func ParseCRD

ParseCRD converts an unstructured HAProxyTemplateConfig CRD to typed structs.

This function validates the resource type, converts the unstructured resource to a typed HAProxyTemplateConfig CRD, and then converts the CRD Spec to a config.Config for validation and rendering.

Returns:

  • *config.Config: Parsed configuration for validation and rendering
  • *v1alpha1.HAProxyTemplateConfig: Original CRD for Kubernetes metadata (name, namespace, UID)
  • error: Validation or conversion failure

func RevisionOf

func RevisionOf(snippets *unstructured.Unstructured) string

RevisionOf reads a snippets object's spec.revision, returning "" when absent so it cannot accidentally equal a reference.

func UnionValidationTests

func UnionValidationTests(sources []ValidationTestSource) (map[string]v1alpha1.ValidationTest, error)

UnionValidationTests combines every source into the single map the test runner consumes.

A test name may appear in only one source. Silently letting the last writer win would leave the losing definition's author believing an assertion runs that does not, so a collision is an error naming both sides.

The reserved `_global` entry is the exception and must be unioned rather than rejected: it is a shared baseline that several template libraries each contribute part of, so "duplicate" is its normal state. Its fixtures accumulate; a field that cannot accumulate (a scalar two sources both set to different values) is still a collision.

Sources are processed in the order given, which fixes the order of accumulated fixtures so a render is reproducible.

Types

type LibraryRef

type LibraryRef struct {
	Name     string
	Revision string
}

LibraryRef is one spec.libraryRefs entry: the snippets object a config pulls in, and the revision it expects that object to report.

func LibraryRefsOf

func LibraryRefsOf(config *unstructured.Unstructured) ([]LibraryRef, error)

LibraryRefsOf reads spec.libraryRefs in declared order. Declared order IS merge order, so this is the only place that order comes from.

type SpecOverride

type SpecOverride struct {
	// Section is the spec section the name lives in (templateSnippets, maps,
	// files, sslCertificates, k8sResources, or haproxyConfig).
	Section string
	Name    string
	// PreviousSource last defined the name before it was replaced.
	PreviousSource string
	// WinningSource defines the entry that ends up in the merged config.
	WinningSource string
}

SpecOverride records one guarded-section name defined by more than one source. An operator overriding a bundled entry is the documented escape hatch and the expected case; it is reported so the override is visible rather than silent.

func MergeSpecs

MergeSpecs merges the .spec of each HAProxyTemplateConfig into one, in argument order, later wins.

The merge primitive is mergo.MergeWithOverwrite — literally the call sprig's `mustMergeOverwrite` makes (vendor/github.com/Masterminds/sprig/v3/dict.go), against the same vendored mergo, starting from the same empty accumulator.

One spec field never reaches mergo:

  • validationTests are unioned per source through UnionValidationTests — error on a non-`_global` duplicate, `_global` contributions accumulate — because a mergo deep-merge of two same-named tests silently fabricates a hybrid test neither author wrote.

templatingSettings.testExtraContext is composed after the merge: the libraries' extraContext beneath every source's testExtraContext.

Within guardedSections, a name defined by two of the first N-1 sources is an error; the LAST source may override anything, each such override is returned, and the override REPLACES the entry — a mergo deep-merge of two same-named entries would blend the operator's fields with library leftovers. haproxyConfig gets the same guard on the whole section: it is one template, not a named map, and a second source setting it replaces the main config outright.

Identity and metadata come from the LAST source — by convention the operator's own config — so status write-back and events target the object an operator edits.

The returned object is safe to hand to ParseCRD; sources are not modified.

type SpecResolution

type SpecResolution struct {
	// StrippedTests maps each stripped validation-test name to a
	// human-readable reason ("requires unavailable resource X" vs
	// "field X absent from resolved schema").
	StrippedTests map[string]string
}

SpecResolution reports what ResolveEffectiveSpec stripped, so the validate CLI can list the stripped tests (the degraded-profile harness asserts the exact set).

func ResolveEffectiveSpec

func ResolveEffectiveSpec(
	spec *v1alpha1.HAProxyTemplateConfigSpec,
	served func(apiVersion, resources string) bool,
	fieldServed func(apiVersion, resources, fieldPath string) (bool, error),
	logger *slog.Logger,
) (*SpecResolution, error)

ResolveEffectiveSpec mirrors the controller's effective-config resolution (pkg/core/config.ResolveEffective) for the offline validate path, operating in place on the CRD spec form the validate CLI's helpers consume. Semantics:

  • each watched resource resolves to the first apiVersions candidate the `served` callback reports as available (in the CLI: a CRD manifest in --schema-dir listing the version as served);
  • an OPTIONAL resource with no served candidate is dropped together with every templateSnippet / validationTest whose requires names it — this is what makes degraded cluster profiles testable offline;
  • a validationTest whose requiresFields names a field the `fieldServed` callback reports absent from the resolved schema is dropped too — the field-level analogue for schema generations that serve the resource at the same version string but without individual fields. fieldServed may be nil (no --schema-dir): field checks are then skipped entirely, the same leniency the untyped path has always had. A fieldServed error fails the resolution (never silently strips);
  • unlike the live controller, a REQUIRED resource with no served candidate falls back to its first candidate instead of failing: offline validation has no cluster to interrogate, and a schema-dir that simply doesn't bundle some watched resource must keep validating through the untyped path exactly as it always has (same leniency as the GVK-resolution skip in the offline type bootstrap). The same fallback applies to every resource when `served` is nil (no --schema-dir at all).

type ValidationTestSource

type ValidationTestSource struct {
	// Origin identifies the contributor in error messages. It is the only way an
	// operator learns which two objects collided, so it must name the object —
	// e.g. "HAProxyv1alpha1.ValidationTests/haptic-config-tests".
	Origin string

	Tests map[string]v1alpha1.ValidationTest
}

ValidationTestSource is one contributor of validation tests: the config's own inline `spec.validationTests`, or a HAProxyv1alpha1.ValidationTests object selected by it.

Jump to

Keyboard shortcuts

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