Skip to main content

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> and AddControllerWithFieldSelector — 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 returns Task<bool>true when the entry was scheduled, false when it was dropped (for example while intake is suspended). await the delegate and handle a false result where it matters.
  • Custom ITimedEntityQueue<TEntity> implementations must return the correct scheduled/dropped result from Enqueue and should implement ISuspendableEntityQueue when combined with LeaderElectionType.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 generated RegisterComponents of 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, EntityDefinitions and EntityInitializer are only generated when the compilation contains matching components. EntityDefinitions only lists entities declared in the compilation itself; entities from referenced assemblies are listed by their declaring assembly. RegisterComponents is generated whenever the compilation references KubeOps.Operator or 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 KubeOpsGeneratorNamespace MSBuild 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-level Program.cs files need a using directive for the configured namespace so that RegisterComponents() resolves.
  • OperatorBuilderExtensions.RegisterComponents and the EntityInitializer.Initialize() extensions are now generated as internal. 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.Scoped partitions responsibility across instances (e.g. by namespace) via a user-registered ILeadershipScope, 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; under QueueStrategy.Custom / LeaderElectionType.Custom only 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 under SharedPerEntity (otherwise standard Kubernetes selector syntax is used).
tip

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.