Releases: sourcehawk/operator-component-framework
v0.16.0
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 onmetadata.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*VersionGateso further conditions compose viaWhen. (#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
goldengencoverage andAssertComplete. The README is trimmed from ~430 to ~116 lines and points at the site. The site builds in--strictmode on every pull request and publishes when a release is published. (#146)
Changelog
- feat(component): add
OrphanWhento release a resource without deleting it (#145) - feat(feature): add
NewBooleanGateconvenience 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
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.WorkloadMutatornow names that shared surface as a single interface, and each
workload package gains aLiftMutation(feature.Mutation[primitives.WorkloadMutator])adapter
that carriesNameandFeaturegating 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
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 declarativeConfig[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).AssertCompleteproves 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 exposeRegisteredMutations()andFiringSet(). It's implemented ongeneric.BaseResource, every primitiveResource, 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
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
ResourceOptionsstruct (or threading aResourceOptionsBuilderwhoseBuild()returned an error, forcing anopts, 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 nowWithResource(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 atBuild()alongside every other error, so the chain stays unbroken. (#126)Migration: replace
WithResources(res, ResourceOptions{ReadOnly: true})-style calls withWithResource(res, ReadOnly()), and drop anyResourceOptionsBuilder/NewResourceOptionsBuilder/ResourceOptionsForusage in favor of the option functions above.
What's new
IncludeWhenfor omit-on-false resources. Before, registering an optional externally-owned resource (say a read-only*SecretKeyRefthat 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: whenincludeis false,buildis 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
ResourceOptionsstruct,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
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
WithMutationfor slice-returning factories. Before, a mutation factory
that returned[]Mutationcouldn't be dropped into a builder chain — you had to break
the chain and loop:for _, m := range factory() { b = b.WithMutation(m) }.
WithMutationnow takes a variadic...Mutation, so you can spread a factory result
directly withWithMutation(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
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.Previewableinterface (#122): New optional capability withPreview() (client.Object, error).generic.BaseResourceimplements it by wrapping the existing no-side-effectPreviewObject()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 itsIdentity()string.golden.CompareComponentYAMLandgolden.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 byPreview() (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 returnedclient.Object(for exampleobj.(*appsv1.Deployment)). The typed enginegeneric.BaseResource.PreviewObject()is retained for internal use and for custom resource wrappers that want a typed preview. goldensingle-resource API is no longer generic (#122):Previewer,CompareYAML, andAssertYAMLdrop the type parameter and operate throughPreview() (client.Object, error). Existinggolden.AssertYAML(t, path, res, opts...)call sites compile unchanged.
Documentation
docs/component.md: New "Previewing Desired State" section coveringComponent.PreviewandComponent.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 withgolden.AssertComponentYAML.
v0.10.0
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.IgnoreIfAbsentandResourceOptionsBuilder.IgnoreIfAbsent()(#120): Opt-in for read-only resources. When set, aNotFoundfrom 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. aSecretowned 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. RequiresReadOnly()and is mutually exclusive withBlockOnAbsence().
Changed
BlockOnAbsence()now requiresReadOnly()atBuild()time (#120): Previously, callingBlockOnAbsence()withoutReadOnly()produced aResourceOptionsstruct where the flag had no effect (silent no-op with only a GoDoc warning). It now returns aBuild()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 theResourceOptionstable forReadOnly: true, IgnoreIfAbsent: true, a new entry in theResourceOptionsBuildermethods table forIgnoreIfAbsent(), and an updatedBlockOnAbsence()row reflecting the tightenedReadOnlyrequirement and the mutual exclusion withIgnoreIfAbsent(). GoDoc onResourceOptions.IgnoreIfAbsentand the builder method spells out the state-preservation semantics.
v0.9.1
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 addedconcepts.ObservationRecorderand dispatched to it via a runtime type assertion against the registered resource. Every primitive'sResourcewrapper 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 tosha256("{}")regardless of cluster contents) reproduced against v0.9.0 for every primitive inpkg/primitives. Each primitive (21 typed plus 4 unstructured variants) now forwardsRecordObservationexplicitly, mirroring howExtractDataandGuardStatusare forwarded.
Added
- Per-primitive compile-time interface assertions: Each primitive's new
observation_test.godeclaresvar _ 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.Resourcevia the public builder withWithDataExtractor, fetches a populated cluster Secret through a fake client, callsRecordObservationandExtractData, 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
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):
readResourcefetched the cluster object into a deep copy that was then discarded, andBaseResource.ExtractDatare-deep-copiedDesiredObject, 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.ObservationRecorderinterface (#115): New interface withRecordObservation(observed client.Object) error.BaseResourceimplements it automatically; primitives inpkg/primitives/pick this up with no changes. Custom resource wrappers built on the generic layer should forwardRecordObservationto their base when read-only data extraction is needed.ResourceOptions.BlockOnAbsenceandResourceOptionsBuilder.BlockOnAbsence()(#114): Opt-in for read-only resources. When set, aNotFoundfrom the cluster is recorded as a blocked status with reasonwaiting 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 theResourceOptionstable and a new entry in theResourceOptionsBuildermethods table forBlockOnAbsence. The reconciliation lifecycle now documents theRecordObservationstep that runs afterGetand before data extraction for read-only resources.docs/custom-resource.md: The typical-methods table and example resource wrapper listRecordObservationalongsideExtractData, with a note explaining when forwarding is required.WithDataExtractorGoDoc clarified for managed versus read-only flows.
v0.8.0
What's Changed
- align guidelines with the framework principles by @sourcehawk in #104
- Add logo to README for visual enhancement by @sourcehawk in #105
- improve opening of the README by @sourcehawk in #106
- add note about the framework's intentions by @sourcehawk in #107
- remove duplication by @sourcehawk in #108
- add mermaid diagram by @sourcehawk in #109
- update owner status outside of component reconciliation by @sourcehawk in #110
Full Changelog: v0.7.1...v0.8.0