typebootstrap

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

Documentation

Overview

Package typebootstrap orchestrates the typed-watched-resources pipeline at controller startup. It's the only place that imports from all three sides:

  • pkg/k8s/schemafetcher — fetches schemas from the cluster
  • pkg/k8s/typegen — translates schemas into reflect.Type
  • pkg/templating — receives the resulting types as engine globals

Rule 1 of arch-go.yml ("controller can import everything") covers this; no other package may do the same coupling.

The pipeline

For each watched resource the operator declared in HAProxyTemplateConfig.spec.watchedResources, the bootstrap:

  1. Resolves the (Group, Version, Resources-plural) triple to a full GVK with Kind via a [GVKResolver] (the production wiring backs this with a RESTMapper).
  2. Calls schemafetcher.Fetcher.Fetch to obtain the resource's OpenAPI v3 schema.
  3. Hands the schema to a typegen.Converter (configured with the merged global + per-resource IgnoreFields list) to produce a reflect.Type.
  4. Stores the resulting type under the resource's user-defined name (the same name templates use to reach `resources.<name>.List()`).

The bootstrap is fail-closed: any single resource whose schema can't be fetched or converted aborts the run with a hard error naming the failing resource. Template authors using typed access (gw.Spec.X, route.Status.Y) need the guarantee that every declared watched resource resolved to its real schema; the previous fail-open-to-envelope path produced silently broken render states with no automatic recovery. Result.Errors still records the per-resource cause for debug surfaces.

The handoff to templating

BuildEngineDeclarations turns the typed-resource map into the shape pkg/templating's Options.Declarations expects — `map[string]any` of typed-nil pointers. Templates compile against those declared types; the actual values arrive at render time from the StoreWrapper (Phase 5, not built yet).

Why a Resource list rather than the CRD config directly

typebootstrap deliberately doesn't import the v1alpha1 config CRD types. The caller (pkg/controller's runIteration) translates HAProxyTemplateConfig.spec.watchedResources into the package's own Resource slice. This keeps typebootstrap testable without the apis package and lets the input shape evolve independently of the CRD schema's churn.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildEngineDeclarations

func BuildEngineDeclarations(result *Result, extraResourceNames ...string) map[string]any

BuildEngineDeclarations turns a Bootstrap Result into the `map[string]any` shape that templating.Options.Declarations expects.

The contract is a single top-level global named "resources" whose type is a dynamically-built struct (reflect.StructOf). The outer struct has one field per watched resource; each inner field is itself a struct holding the chart-facing access surface:

resources struct {
    Widgets *widgetStore `json:"widgets"`
    FooBars *fooBarStore `json:"foobars"`
    ...
}

widgetStore struct {
    List      func() []*Widget
    Fetch     func(keys ...any) []*Widget
    GetSingle func(keys ...any) *Widget
}

Chart templates reach the typed-iteration path via `resources.widgets.List()` and the indexed-lookup paths via `resources.widgets.GetSingle(ns, name)` / `resources.widgets.Fetch(...)`. The return values are typed `*Widget` / `[]*Widget`, so Scriggo's type-checker validates field access (`w.Metadata.Namespace`) at engine boot. (Names here are generic placeholders — the field set is whatever the operator watches, never a fixed list of well-known kinds.)

Resources whose schema didn't resolve (entry in Result.Errors) still get an inner store struct, with the same field names, but the closure return types collapse to `[]any` / `any`. Chart code reaches them with identical syntax — just no compile-time field validation on the element shape.

The closure VALUES live in rendercontext: typebootstrap only declares the *types*. Scriggo's typed-nil-pointer declaration pattern (the outer `(*Resources)(nil)` here) carries the type through engine compilation; the runtime binding fills in the closures per render.

Field naming: outer Go field name = PascalCase of the resource key (`gateways` → `Gateways`); json tag = the lower-case wire-form resource name, so the chart can write `resources.gateways` and have the Scriggo json-tag selector fallback route the lowercase access to the PascalCase field. Inner field names are `List` / `Fetch` / `GetSingle` (no json tag — chart writes PascalCase directly).

`extraResourceNames` carries the watched-resource names that are NOT in result.Types or result.Errors — typically core Kubernetes types (Service, Secret, ConfigMap, Pod) for which the controller has no typegen-derived schema available, but the chart still watches them and templates still reach `resources.<name>`. These names get untyped struct fields so the engine-declared shape stays in lockstep with the runtime-populated value built by rendercontext.

Resources are listed in sorted order so the struct layout is deterministic across boots.

Returns a map with one entry keyed "resources". Empty input (no watched resources at all) yields an empty map; callers should merge without special-casing.

func BuildPerResourceStoreType

func BuildPerResourceStoreType(elemType reflect.Type) reflect.Type

BuildPerResourceStoreType is the exported entry point rendercontext uses to build the same per-resource store struct shape declared here. Keeping this in one place ensures the engine-declared type and the render-time value type match byte-for-byte (different types of the same shape compare unequal to reflect, so any drift would surface as "wrong type" errors at template bind time).

When `elemType` is non-nil, the store struct's methods are typed for `*elemType`. When nil, they fall back to untyped `any` / `[]any` — used for watched resources whose schema bootstrap failed.

Types

type Config

type Config struct {
	// Resources lists every watched-resource entry to type-resolve.
	// Order doesn't matter; results are keyed by [Resource.Name].
	Resources []Resource

	// GlobalIgnoreFields is the cluster-wide ignore list (from
	// HAProxyTemplateConfig.spec.watchedResourcesIgnoreFields).
	// Each per-resource Bootstrap call merges this with the
	// resource's own [Resource.IgnoreFields] before invoking
	// typegen.
	GlobalIgnoreFields []string

	// Fetcher is the schema-acquisition strategy. Production wires
	// this to [schemafetcher.NewClusterFetcher]; tests use a
	// [schemafetcher.MapFetcher] with pre-baked schemas.
	Fetcher schemafetcher.Fetcher

	// Logger receives the structured error log for the resource
	// whose schema acquisition failed. Required — bootstrap is the
	// only surface that observes per-resource schema problems
	// before they abort iteration startup, so operators need
	// visibility into which resource (and why) caused the failure.
	Logger *slog.Logger
}

Config wraps the inputs Bootstrap needs.

type OfflineGVKResolver

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

OfflineGVKResolver maps (apiVersion, resources-plural) pairs to fully qualified GVKs without consulting an API server. The production wiring uses a RESTMapper built from the cluster's discovery; the offline validate path has no cluster, so it threads this through instead.

The mapping is never derived from a pluralisation rule: kinds have too many irregular and fully custom plurals to guess reliably, so the resolver takes its entries from authoritative schema data rather than guessing.

The resolver starts empty. Callers populate it from the user's `--schema-dir`: every CRD YAML in the directory contributes its `spec.names.plural` → GVK mapping, and bare OpenAPI v3 schemas with an `x-kubernetes-group-version-kind` extension contribute theirs. Resources without a matching entry surface as a not-found error pointing the operator at `--schema-dir`. The offline validate caller (cmd/controller/validate.go) skips unresolved entries before passing the list to Bootstrap, so they never reach the fail-closed schema-fetch path — the chart still validates for them through dig().

func NewOfflineGVKResolver

func NewOfflineGVKResolver() *OfflineGVKResolver

NewOfflineGVKResolver returns an empty resolver. Callers populate the (apiVersion, resources-plural) → GVK mapping via Register, most commonly by walking a `--schema-dir` and emitting one Register per CRD spec.names.plural entry.

An empty resolver is a valid state: configs without typed `watchedResources` (or with watched resources whose schemas the operator chose not to supply) Bootstrap to a zero Result and the chart validates entirely through dig() on the untyped resources map.

func (*OfflineGVKResolver) Register

func (r *OfflineGVKResolver) Register(apiVersion, resources string, gvk schema.GroupVersionKind) *OfflineGVKResolver

Register adds (or overrides) a single (apiVersion, resources-plural) → GVK mapping. Returns the receiver for chaining in test setup.

func (*OfflineGVKResolver) Resolve

func (r *OfflineGVKResolver) Resolve(apiVersion, resources string) (schema.GroupVersionKind, error)

Resolve returns the fully qualified GVK for the supplied (apiVersion, resources-plural) pair. Missing entries return an error pointing the operator at `--schema-dir`; the bootstrap caller logs and degrades that single resource.

type Resource

type Resource struct {
	// Name is the user-defined identifier templates use to reach
	// this resource — the same key that appears as
	// `resources.<Name>` in chart templates.
	Name string

	// GVK is the fully-resolved (Group, Version, Kind) triple. The
	// caller is responsible for translating the CRD-side
	// (apiVersion, resources-plural) form into Kind via a
	// RESTMapper before handing the resource to typebootstrap;
	// this keeps the package free of REST-mapping deps.
	GVK schema.GroupVersionKind

	// IgnoreFields is the per-resource ignore list. Merged with
	// [Config.GlobalIgnoreFields] before being passed to typegen,
	// matching the merge the resourcewatcher does for its runtime
	// FieldFilter (pkg/controller/resourcewatcher/watcher.go
	// `mergeIgnoreFields`). Order of the inputs doesn't matter —
	// typegen deduplicates effectively by storing patterns in a
	// set.
	IgnoreFields []string
}

Resource describes one watched-resource entry that typebootstrap should resolve into a generated Go type. Callers build this slice from their config of choice (the production wiring derives it from HAProxyTemplateConfig.spec.watchedResources, but tests build it directly).

type Result

type Result struct {
	// Types maps the resource's user-defined name to its generated
	// Go type. Pass these to [BuildEngineDeclarations] to produce
	// the engine's additionalDeclarations map.
	Types map[string]reflect.Type

	// Kinds maps the resource's user-defined name to the GVK.Kind
	// string captured from cfg.Resources. Used by
	// [BuildEngineDeclarations] to name the per-resource type
	// declarations (one per watched Kind — e.g. for a CRD named
	// `Widget` the declaration is `Widget`) at engine-declaration
	// time so chart authors can write typed macro signatures
	// (`(w *Widget)`, `(items []*Widget)`) instead of `any`/`[]any`.
	//
	// The Kind is the canonical singular noun for the resource —
	// K8s naming preserves acronyms (HTTPRoute, BackendTLSPolicy)
	// which reads more naturally in chart macros than the
	// PascalCase-of-resource-key form (Httproutes,
	// Backendtlspolicies). Multiple watched resources sharing a
	// Kind (e.g. `services` and `controller_services` both → Kind
	// "Service") deduplicate to a single type declaration; the
	// chart picks up the shared declaration regardless of which
	// watched-resource key carries the schema.
	Kinds map[string]string

	// Errors records why a resource didn't get a typed view.
	// Bootstrap is fail-closed (see Bootstrap doc): the first
	// per-resource failure aborts the run, so Errors typically
	// has at most one entry — but defensively it's a map so
	// downstream code that wants to enumerate is uniform with
	// successful runs.
	Errors map[string]error
}

Result is what Bootstrap returns. Types holds the successful resolutions Bootstrap completed before any failure; Errors holds the per-resource cause for any failure that aborted the run. Both maps are always non-nil so callers can range them without nil checks.

func Bootstrap

func Bootstrap(ctx context.Context, cfg Config) (*Result, error)

Bootstrap orchestrates the schema-fetch → type-generate pipeline for every resource in cfg.Resources. Returns a *Result aggregating successful types and per-resource errors. The outer error is reserved for catastrophic failures (missing required config); a resource that individually fails goes into Result.Errors so the chart can keep booting with reduced typing rather than refusing to start.

Concurrency note: this currently runs sequentially. The fetcher already coalesces concurrent calls per GroupVersion / CRD list (schemafetcher's cluster fetcher uses sync.Once + per-GV done channels), and the number of watched resources is small (≤20 on any cluster we've seen), so parallelising the loop here doesn't pay off. If a future controller grows the resource count by an order of magnitude this becomes worth revisiting.

Jump to

Keyboard shortcuts

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