Migration Guide
KubeOps releases follow semantic versioning: breaking changes only happen in
major versions, and every breaking change is flagged in the
release notes (commits follow
Conventional Commits, so breaking PRs are marked with !
or a BREAKING CHANGE footer). This page summarizes the changes that require action when
upgrading, starting with v11.
Upgrading from v10 to v11
Selector namespace renamed
The LabelSelectors namespace was renamed to Selectors (it now also hosts the field selector
types).
// before (v10)
using KubeOps.KubernetesClient.LabelSelectors;
// after (v11)
using KubeOps.KubernetesClient.Selectors;
Migration: rename the namespace in your using directives.
DefaultEntityLabelSelector<TEntity> is sealed
Subclassing the default label selector is no longer possible.
Migration: implement IEntityLabelSelector<TEntity> directly (the intended extension point)
and register it via
AddControllerWithLabelSelector:
public class MySelector : IEntityLabelSelector<V1DemoEntity>
{
public ValueTask<string?> GetLabelSelectorAsync(CancellationToken cancellationToken) =>
ValueTask.FromResult<string?>("app=my-operator");
}
New in v11 (non-breaking)
- Field selectors: watches can now be filtered server-side by resource fields via
IEntityFieldSelector<TEntity>andAddControllerWithFieldSelector— see Filtering Watched Resources. - YamlDotNet 18: the CLI/transpiler dependency was upgraded across v11 releases. If your project serializes YAML itself and pins an older YamlDotNet, align the versions.
Upgrading from v11 to v12
Leader-loss-aware queue and reconciliation
With LeaderElectionType.Single, the reconciliation queue is now gated on leadership: on
leadership loss the queue's intake is suspended, in-flight reconciliations are cancelled, and
pending entries are cleared (see
Behavior on Leadership Loss).
Action required if you use custom queue components:
- The
EntityQueue<TEntity>delegate (formerly fire-and-forget) now returnsTask<bool>—truewhen the entry was scheduled,falsewhen it was dropped (for example while intake is suspended).awaitthe delegate and handle afalseresult where it matters. - Custom
ITimedEntityQueue<TEntity>implementations must return the correct scheduled/dropped result fromEnqueueand should implementISuspendableEntityQueuewhen combined withLeaderElectionType.Single— see Queue Strategy. - Reconcilers must honor their
CancellationToken, since leadership loss now cancels in-flight reconciliations.
Upgrading from v12 to v13
Kubernetes client list methods accept field selectors
IKubernetesClient.List and ListAsync now accept an optional fieldSelector after the existing
labelSelector, matching the parameter order of the upstream Kubernetes client:
client.ListAsync<TEntity>(
@namespace,
labelSelector,
fieldSelector,
cancellationToken);
This changes the positional asynchronous overload. Calls that previously passed the cancellation token as the third positional argument must name it explicitly:
// before (v12)
client.ListAsync<TEntity>(@namespace, labelSelector, cancellationToken);
// after (v13)
client.ListAsync<TEntity>(@namespace, labelSelector, cancellationToken: cancellationToken);
The typed params LabelSelector[] overloads of List, ListAsync, and Watch now accept
params KubernetesSelector[], and the parameter was renamed from labelSelectors to selectors.
Positional label-selector calls remain source-compatible, while label and field selectors can now
be combined in one call:
client.List<TEntity>(
@namespace,
new EqualsLabelSelector("app", "demo"),
new EqualsFieldSelector("metadata.name", "demo"));
Callers that passed the selectors by name must rename the argument, because the parameter is no
longer called labelSelectors:
// before (v12)
client.List<TEntity>(@namespace, labelSelectors: new EqualsLabelSelector("app", "demo"));
// after (v13)
client.List<TEntity>(@namespace, selectors: new EqualsLabelSelector("app", "demo"));
The synchronous string-based Watch overload likewise accepts fieldSelector immediately after
labelSelector, matching List, ListAsync, and WatchAsync.
Generator multi-assembly composition
The source generator now supports multi-assembly composition: controllers, finalizers and
entities may live in referenced class libraries that also reference KubeOps.Generator.
- Assemblies containing controllers or finalizers are marked with an assembly-level
KubeOpsGeneratedRegistrationsAttribute. The generatedRegisterComponentsof the consuming compilation discovers these markers on all transitively referenced assemblies and registers every assembly's components exactly once — components in referenced class libraries are now registered automatically. ControllerRegistrations,FinalizerRegistrations,EntityDefinitionsandEntityInitializerare only generated when the compilation contains matching components.EntityDefinitionsonly lists entities declared in the compilation itself; entities from referenced assemblies are listed by their declaring assembly.RegisterComponentsis generated whenever the compilation referencesKubeOps.Operatoror contains/references components, and only invokes the parts that exist.- The shared generated classes remain in the global namespace by default. When multiple
projects of one solution use the generator, set the new
KubeOpsGeneratorNamespaceMSBuild property (e.g. to$(RootNamespace)) in the participating projects so the generated class names do not conflict. The generator reports the warning KOG002 and skips the conflicting registration when it detects such a clash. Note that with the property set, top-levelProgram.csfiles need a using directive for the configured namespace so thatRegisterComponents()resolves. OperatorBuilderExtensions.RegisterComponentsand theEntityInitializer.Initialize()extensions are now generated asinternal. They are only meaningful for the compilation they are generated into; keeping them invisible across assembly boundaries rules out ambiguous extension method calls when multiple assemblies use the generator. This only affects the exotic case of calling these generated extensions from a different assembly — such setups were already broken by ambiguous calls as soon as two projects referenced the generator. If another assembly relied on calling them, reference the generator in that assembly instead — it generates its own.
v13 makes it possible to register multiple controllers for the same entity type, each with its
own label or field selector (see
Multiple Controllers per Entity).
Delivering this required reworking the internal dispatch pipeline: every controller registration now
owns a dedicated ControllerPipeline<TEntity> (watcher + queue + consumer). Most operators need no
code changes — the breaking changes below only affect projects that implement custom queue,
watcher, or reconciler components, or that resolve those internals from DI.
IEntityQueueFactory / EntityQueueFactory removed
The queue-factory abstraction is gone. Requeues are now routed through the active pipeline by the
EntityQueue<TEntity> delegate, which resolves the queue of the pipeline currently reconciling the
entity.
Migration: remove references to IEntityQueueFactory / EntityQueueFactory. To schedule a
requeue from a controller, return
ReconciliationResult<TEntity>.Success(entity, requeueAfter)
instead of touching the queue directly. If you injected EntityQueue<TEntity> to enqueue manually,
keep using the delegate — but note the resolution change below.
EntityQueue<TEntity> no longer resolvable from a singleton with multiple pipelines
Because each pipeline owns its own queue, there is no single "the queue" for an entity type when more
than one controller is registered for it. Resolving EntityQueue<TEntity> from a singleton now
throws an InvalidOperationException instead of silently handing out a shared queue.
Migration: resolve and invoke EntityQueue<TEntity> from within a reconciliation scope (where
the active pipeline is known), or from a scoped/transient service. Operators with a single controller
per entity are unaffected.
Custom watcher constructors gained a cachePartition parameter
ResourceWatcher<TEntity> and LeaderAwareResourceWatcher<TEntity> take an additional optional
cachePartition argument. It partitions the generation-dedup cache per pipeline so that overlapping
selectors no longer let one pipeline's cache write suppress another pipeline's event.
Migration: if you subclass or construct these watchers directly, thread the new parameter through (it defaults to a shared partition, preserving single-pipeline behavior). Pass a distinct token per pipeline when running several watchers for the same entity type.
EntityQueueBackgroundService<TEntity> constructor gained a coordinator parameter
The queue consumer's constructor takes an additional IEntityReconcileCoordinator<TEntity>. The
coordinator is a per-entity-type singleton that owns the global parallelism budget and the per-UID
exclusive lock, so multiple controllers for the same entity no longer multiply the parallelism limit
and never reconcile (or finalize) the same object concurrently.
Migration: if you derive a custom consumer from EntityQueueBackgroundService<TEntity> (or
LeaderAwareEntityQueueBackgroundService<TEntity>), forward the new IEntityReconcileCoordinator<TEntity>
to the base constructor — exactly as you already forward IReconciler<TEntity>. It is registered
automatically per entity type, so constructor injection resolves it.
IReconciler<TEntity> / ITimedEntityQueue<TEntity> not registered for the in-memory strategy
With the default in-memory queue strategy these services are now owned by the pipeline and are no
longer registered in the DI container. Registration is unchanged for LeaderElectionType.Custom
and QueueStrategy.Custom, where you still wire up the queue and consumer yourself (the SDK keeps
registering IReconciler<TEntity> for these paths, so your consumer can resolve it).
Migration: stop resolving IReconciler<TEntity> / ITimedEntityQueue<TEntity> from DI in the
default configuration. If you need custom queue behavior, opt into
QueueStrategy.Custom and register your implementation there.
Reconciler<TEntity> now carries a controllerType
The internal Reconciler<TEntity> receives a Type controllerType so it can resolve the correct
controller for its pipeline by concrete type. This is an internal type; only projects that
constructed it directly are affected.
Migration: pass the concrete controller type when constructing Reconciler<TEntity> yourself, or
rely on the SDK's registration.
Entity logging scopes are created through a factory
ResourceWatcher<TEntity>, EntityQueueBackgroundService<TEntity> and their leader-/scope-aware
derivations take an additional IEntityLoggingScopeFactory<TEntity> argument. The factory applies all
registered enrichers while creating the scope (see
Logging).
Migration: resolve IEntityLoggingScopeFactory<TEntity> through dependency injection. Use its
CreateFor(...) methods to create an enriched scope, and pass the factory when directly constructing or
subclassing the watcher and queue types. EntityLoggingScope.CreateFor(...) remains available for standalone,
unenriched scopes. The default factory is registered automatically per entity type and is a no-op beyond the
built-in fields when no enrichers are configured.
New in v13 (non-breaking)
- Scoped leader election:
LeaderElectionType.Scopedpartitions responsibility across instances (e.g. by namespace) via a user-registeredILeadershipScope, while keeping the full default pipeline — see Scoped Leader Election. - Multiple controllers per entity: register several
IEntityController<TEntity>for the same entity with different selectors — both fire for objects matching both selectors. Supported on the default in-memory queue with non-custom leader election; underQueueStrategy.Custom/LeaderElectionType.Customonly one controller per entity is allowed (a second registration throws). See Multiple Controllers per Entity. - Watch strategy: choose how watch streams are provisioned via
OperatorSettings.WatchStrategy/WithWatchStrategy(...).WatchStrategy.PerController(default) gives every controller its own server-side–filtered watch;WatchStrategy.SharedPerEntity(opt-in) shares a single watch stream per entity type and matches events client-side. - Selector attributes: declare a controller's selector with
[LabelSelector(typeof(...))]/[FieldSelector(typeof(...))]; the source generator emits the matching registration. Declaring both on one controller is now a compile-time error (KOG001) instead of silently registering only the label selector — split it into two controllers. IClientSideEntitySelector<TEntity>: implement it on a selector to control client-side matching underSharedPerEntity(otherwise standard Kubernetes selector syntax is used).
Every breaking change is also listed in the release notes. Check the notes for the version you are upgrading to when something does not match this guide.