Skip to content

Releases: sourcehawk/operator-component-framework

v0.16.0

02 Jun 03:15
0ed5eee

Choose a tag to compare

Overview

This release adds two ergonomic building blocks for operator authors and ships a complete documentation site. feature.NewBooleanGate and component.OrphanWhen each close a small but recurring gap in everyday operator code, and the documentation is now published as a searchable Material for MkDocs site on GitHub Pages with every page rewritten and verified against source. There are no breaking changes; upgrading is a drop-in.

What's new

  • Release a resource without deleting it. Migrating a managed resource to a new owner used to be impossible to do cleanly: the controller owner reference both pinned the object to garbage collection under the old owner and blocked adoption by a new one, and the only built-in way to stop managing a resource was to delete it. component.OrphanWhen(cond) now strips just this owner's controller reference when the condition is true, leaving the object intact in the cluster so a new owner can adopt it. It uses a fetch-and-update on metadata.ownerReferences (not a Server-Side Apply), so every other field is preserved, and it is idempotent and safe across the normal, suspended, and feature-gate-disabled reconcile paths. (#145)

  • A direct constructor for boolean-driven gates. Toggling a mutation or resource on a plain spec flag rather than an application version meant spelling it feature.NewVersionGate("", nil).When(enabled) — obscure to read and easy to mistype. feature.NewBooleanGate(enabled) gives that common case a named constructor, and it still returns *VersionGate so further conditions compose via When. (#144)

What's changed

  • Documentation is now a published site. The docs moved from in-repo Markdown to a Material for MkDocs site on GitHub Pages, with instant search, dark mode, content tabs, and Mermaid diagrams. Every page — Component, Primitives overview and all 22 primitive references, Custom Resources, Guidelines, Testing, Compatibility — was rewritten to one standard with API references checked against source. New: a landing page and a hands-on Getting Started tutorial grounded in a runnable example. The Testing guide is restructured around the three layers (mutation, resource, component) with goldengen coverage and AssertComplete. The README is trimmed from ~430 to ~116 lines and points at the site. The site builds in --strict mode on every pull request and publishes when a release is published. (#146)
Changelog
  • feat(component): add OrphanWhen to release a resource without deleting it (#145)
  • feat(feature): add NewBooleanGate convenience constructor (#144)
  • docs: publish documentation as a GitHub Pages site and run a full quality pass (#146)

Full diff: v0.15.0...v0.16.0

v0.15.0

01 Jun 20:28
6f3c796

Choose a tag to compare

Overview

A small, additive release for operators that render the same concern across more than one
pod-workload kind. It exports primitives.WorkloadMutator, the editing surface shared by the
StatefulSet, Deployment, and DaemonSet mutators, so a single mutation can be written once and
applied to any of them. There are no behavior changes and nothing to migrate. Upgrade when you
have a mutation you'd otherwise have to duplicate per workload kind.

What's new

  • Workload-kind-agnostic mutations. Before this release, the StatefulSet, Deployment, and
    DaemonSet mutators exposed an almost identical container/env/podspec/metadata editing surface
    but as unrelated concrete types, so a component that shared an env-emission concern across (say)
    a StatefulSet and a Deployment had to write the mutation twice against two framework types.
    primitives.WorkloadMutator now names that shared surface as a single interface, and each
    workload package gains a LiftMutation(feature.Mutation[primitives.WorkloadMutator]) adapter
    that carries Name and Feature gating through unchanged. One emitter, applied to any
    pod-workload kind. (#143)

  • Compile-time conformance guards. Each mutator package now asserts
    var _ primitives.WorkloadMutator = (*Mutator)(nil), so a future rename or removal of a shared
    method breaks the build inside the framework rather than drifting silently into downstream
    consumers. (#143)

Changelog
  • feat(primitives): shared WorkloadMutator interface for workload-kind-agnostic mutations (#143)

Full diff: v0.14.0...v0.15.0

v0.14.0

01 Jun 12:03
249f04b

Choose a tag to compare

Overview

v0.14.0 adds version-matrix golden generation: a test-only toolkit for proving that version-gated mutations fire exactly where you think they do, across an entire version universe, rather than at a few hand-picked points. If you maintain mutations behind version gates and verify them with golden snapshots, this release closes a class of silent coverage gaps. The framework additions are read-only and inert unless you call them, so upgrading is safe for everyone else.

What's new

  • Version-matrix golden generation (pkg/testing/goldengen). Before, consumers verified version-gated mutations with goldens hand-picked at a few versions. That approach silently missed regime gaps (a mutation correct at 8.9 but a no-op at 8.8) and never actually proved a mutation fired where it was assumed to. A single declarative Config[T] now sweeps every fixture across every version, classifies versions into behaviorally-distinct regimes by their firing-set, writes one reviewable golden per regime, and asserts each fixture's gating expectations (Requires/Forbids). AssertComplete proves every registered mutation is either exercised by a fixture or explicitly excluded, so a new gated mutation can't slip in untested. (#140)

  • Mutation introspection (concepts.MutationInspector). The matrix toolkit needs to know which mutations a resource registers and which fire at a given version, so resources now expose RegisteredMutations() and FiringSet(). It's implemented on generic.BaseResource, every primitive Resource, and *component.Component (which unions across its managed resources). The framework stays black-box about gating: it never parses version constraints, only evaluates the gate and classifies by firing-set. (#140)

  • Exported golden serialization (golden.Serialize / golden.SerializeComponent). Generated goldens are now byte-identical to hand-written ones, because both go through the same serializer the golden helpers already used. You can mix generated and hand-authored snapshots without diff noise. (#140)

Changelog
  • Version-matrix golden generation: classify versions by firing-set and assert mutation coverage (#140)

Full diff: v0.13.0...v0.14.0

v0.13.0

31 May 02:41
b60ce40

Choose a tag to compare

Overview

This is a small but breaking release for anyone registering resources on a component. The struct-and-separate-builder API for resource options is gone, replaced by functional options on the component builder, plus a new IncludeWhen for registering optional resources that may not exist. If you call WithResources with ResourceOptions or use ResourceOptionsBuilder, you'll need to migrate your call sites; the change is mechanical but unavoidable. Everyone else gets a cleaner, fully-fluent builder chain.

Breaking changes

  • Resource registration moves to functional options. Previously, attaching options to a resource meant constructing a ResourceOptions struct (or threading a ResourceOptionsBuilder whose Build() returned an error, forcing an opts, err := ... break in the middle of an otherwise-fluent builder chain). That error path also surfaced option-resolution failures at a different point than every other builder error. Resource registration is now WithResource(resource, opts...), where each option is a function: ReadOnly(), Delete(), DeleteWhen(cond), GatedBy(gate), Auxiliary(), BlockOnAbsence(), IgnoreIfAbsent(), SuppressGraceInconsistencyWarning(). Option-resolution errors (gate-eval failures, invalid flag combinations) now aggregate into the builder and surface at Build() alongside every other error, so the chain stays unbroken. (#126)

    Migration: replace WithResources(res, ResourceOptions{ReadOnly: true})-style calls with WithResource(res, ReadOnly()), and drop any ResourceOptionsBuilder / NewResourceOptionsBuilder / ResourceOptionsFor usage in favor of the option functions above.

What's new

  • IncludeWhen for omit-on-false resources. Before, registering an optional externally-owned resource (say a read-only *SecretKeyRef that might be nil) meant guarding the registration yourself, because the resource argument is constructed before any option runs: a nil deref would panic before a gate could stop it. IncludeWhen(include, func() Resource, opts...) defers construction behind a closure: when include is false, build is never called, and the resource is never created, read, deleted, or counted in the duplicate-Identity check. Optional resources can now be registered inline without a manual nil guard. (#126)

What's removed

  • ResourceOptions struct, ResourceOptionsBuilder, NewResourceOptionsBuilder, ResourceOptionsFor. Replaced by the functional options above. The resolved value type is now internal. Migrate per the breaking-changes section. (#126)
Changelog
  • feat(component): functional-option resource registration (#126)

Full diff: v0.12.0...v0.13.0

v0.12.0

30 May 23:09
eb40723

Choose a tag to compare

Overview

Routine minor release. The headline is a quality-of-life improvement to the builder
API: WithMutation now accepts multiple mutations at once, so mutation factories that
return a slice compose cleanly inside a fluent chain. The change is a pure widening —
every existing call compiles and behaves exactly as before — so upgrading is safe with
no code changes required. The remainder is dependency maintenance.

What's new

  • Variadic WithMutation for slice-returning factories. Before, a mutation factory
    that returned []Mutation couldn't be dropped into a builder chain — you had to break
    the chain and loop: for _, m := range factory() { b = b.WithMutation(m) }.
    WithMutation now takes a variadic ...Mutation, so you can spread a factory result
    directly with WithMutation(factory()...) and keep the chain fluent. Mutations
    register in argument order, and a zero-argument call is a no-op. The widening covers
    all 5 generic builders and all 25 primitive builders. (#124)
Changelog
  • feat(builders): variadic WithMutation for slice-returning factories (#124)
  • Update module github.com/Masterminds/semver/v3 to v3.5.0 (#111)
  • Update test-dependencies (#103)
  • Update k8s.io/utils digest to ff6756f (#113)

Full diff: v0.11.0...v0.12.0

v0.11.0

29 May 19:05
f63f8aa

Choose a tag to compare

This release adds a public, cluster-free way to render a component's desired-state resources for whole-component golden tests, and collapses each primitive's typed preview into a single polymorphic method.

Added

  • concepts.Previewable interface (#122): New optional capability with Preview() (client.Object, error). generic.BaseResource implements it by wrapping the existing no-side-effect PreviewObject() engine, so all built-in primitives satisfy it automatically.
  • Component.Preview() ([]client.Object, error) (#122): Renders the desired state of every managed resource registered on the component, in registration order, without contacting the cluster. Read-only resources (fetched, not applied) and delete resources (removal markers) are excluded. Guards are not evaluated, because a guard outcome depends on cluster state and on data extracted from earlier resources, so the result reports the full desired set and stays deterministic. Returns an error if a managed resource is not previewable, renders a nil object, or fails to render.
  • Component.Resource(identity string) (Resource, bool) (#122): Looks up any registered resource (managed, read-only, or delete) by its Identity() string.
  • golden.CompareComponentYAML and golden.AssertComponentYAML (#122): Serialize every resource a component would apply into one multi-document YAML golden file (documents joined by --- separators, in registration order).

Changed (breaking)

  • Primitive PreviewObject() (*Concrete, error) replaced by Preview() (client.Object, error) (#122): Each built-in primitive now exposes a single type-erased preview method instead of the typed one, so a component can render a mixed resource set polymorphically. Callers that need the concrete Kubernetes type assert on the returned client.Object (for example obj.(*appsv1.Deployment)). The typed engine generic.BaseResource.PreviewObject() is retained for internal use and for custom resource wrappers that want a typed preview.
  • golden single-resource API is no longer generic (#122): Previewer, CompareYAML, and AssertYAML drop the type parameter and operate through Preview() (client.Object, error). Existing golden.AssertYAML(t, path, res, opts...) call sites compile unchanged.

Documentation

  • docs/component.md: New "Previewing Desired State" section covering Component.Preview and Component.Resource, including the guard semantics and the namespaced versus cluster-scoped identity formats.
  • docs/guidelines.md: The backward-compatibility golden section documents whole-component snapshots with golden.AssertComponentYAML.

v0.10.0

28 May 08:57
81fe143

Choose a tag to compare

This release adds a third NotFound mode for read-only resources, IgnoreIfAbsent, for resources that are genuinely optional, and tightens validation on the existing BlockOnAbsence flag.

Added

  • ResourceOptions.IgnoreIfAbsent and ResourceOptionsBuilder.IgnoreIfAbsent() (#120): Opt-in for read-only resources. When set, a NotFound from the cluster is silently ignored: the entry is skipped, no condition or observation is recorded, the data extractor is not invoked, and reconciliation of subsequent resources continues unchanged. Intended for references to resources that may legitimately be absent (e.g. a Secret owned by another operator). State recorded in earlier reconciles (last observation, extracted data) is preserved across an absence rather than reset, so downstream consumers see the last-known value until a future reconcile finds the resource present again. Requires ReadOnly() and is mutually exclusive with BlockOnAbsence().

Changed

  • BlockOnAbsence() now requires ReadOnly() at Build() time (#120): Previously, calling BlockOnAbsence() without ReadOnly() produced a ResourceOptions struct where the flag had no effect (silent no-op with only a GoDoc warning). It now returns a Build() error. The two NotFound flags (BlockOnAbsence, IgnoreIfAbsent) are also rejected if both are set. Any caller relying on the previous silent no-op was almost certainly misconfigured; the error surfaces the misconfiguration at construction instead of at runtime.

Documentation

  • docs/component.md: New row in the ResourceOptions table for ReadOnly: true, IgnoreIfAbsent: true, a new entry in the ResourceOptionsBuilder methods table for IgnoreIfAbsent(), and an updated BlockOnAbsence() row reflecting the tightened ReadOnly requirement and the mutual exclusion with IgnoreIfAbsent(). GoDoc on ResourceOptions.IgnoreIfAbsent and the builder method spells out the state-preservation semantics.

v0.9.1

24 May 17:38
d42ea51

Choose a tag to compare

This release fixes a regression in v0.9.0 where the read-only data-extraction fix shipped in that version was inert for every primitive in pkg/primitives.

Fixed

  • Primitive resource wrappers now forward RecordObservation (#118): The v0.9.0 fix for #115 added concepts.ObservationRecorder and dispatched to it via a runtime type assertion against the registered resource. Every primitive's Resource wrapper holds its underlying generic resource behind an unexported named field rather than embedding it, so methods are not promoted onto the wrapper. The type assertion therefore failed for every primitive, the fetched object was never recorded, and read-only data extractors continued to see the inert base used to build the resource. The original #115 scenario (Secret rotation hash equal to sha256("{}") regardless of cluster contents) reproduced against v0.9.0 for every primitive in pkg/primitives. Each primitive (21 typed plus 4 unstructured variants) now forwards RecordObservation explicitly, mirroring how ExtractData and GuardStatus are forwarded.

Added

  • Per-primitive compile-time interface assertions: Each primitive's new observation_test.go declares var _ concepts.ObservationRecorder = (*Resource)(nil). A primitive that forgets to forward the method fails to build instead of regressing silently. The same pattern can be extended to lock in any future framework-side type-assertion dispatch.
  • End-to-end regression test for secret (pkg/primitives/secret/observation_test.go): Builds a real *secret.Resource via the public builder with WithDataExtractor, fetches a populated cluster Secret through a fake client, calls RecordObservation and ExtractData, and asserts the extractor observes the live data. Catches both forwarder-missing and forwarder-broken regressions for the canonical user-reported scenario.

v0.9.0

24 May 16:17
94185a2

Choose a tag to compare

This release fixes a bug in read-only resource data extraction and adds an opt-in for treating absent read-only resources as guard-blocked instead of erroring.

Fixed

  • Read-only resource extractors now observe the fetched cluster object (#115): readResource fetched the cluster object into a deep copy that was then discarded, and BaseResource.ExtractData re-deep-copied DesiredObject, so extractors on read-only resources only ever saw the inert base used to build the resource. The framework now records the fetched object back onto the resource before extraction runs, so closures capturing values from read-only Secrets or ConfigMaps see real cluster state.

Added

  • concepts.ObservationRecorder interface (#115): New interface with RecordObservation(observed client.Object) error. BaseResource implements it automatically; primitives in pkg/primitives/ pick this up with no changes. Custom resource wrappers built on the generic layer should forward RecordObservation to their base when read-only data extraction is needed.
  • ResourceOptions.BlockOnAbsence and ResourceOptionsBuilder.BlockOnAbsence() (#114): Opt-in for read-only resources. When set, a NotFound from the cluster is recorded as a blocked status with reason waiting for <resource> and short-circuits the remaining resources for the reconcile, instead of returning an error and triggering controller-runtime's exponential backoff. Intended for consumers that have a watch on the referenced resource type so that re-enqueue happens through the watch rather than the backoff loop. The flag has no effect on managed resources.

Documentation

  • docs/component.md: New row in the ResourceOptions table and a new entry in the ResourceOptionsBuilder methods table for BlockOnAbsence. The reconciliation lifecycle now documents the RecordObservation step that runs after Get and before data extraction for read-only resources.
  • docs/custom-resource.md: The typical-methods table and example resource wrapper list RecordObservation alongside ExtractData, with a note explaining when forwarding is required.
  • WithDataExtractor GoDoc clarified for managed versus read-only flows.

v0.8.0

13 May 11:06
7e4f93d

Choose a tag to compare

What's Changed

Full Changelog: v0.7.1...v0.8.0