Skip to content

fix(operator-web): compose conversions through the storage version#1194

Merged
kimpenhaus merged 3 commits into
dotnet:mainfrom
alethic:fix/conversion-webhook-multi-hop
Jul 12, 2026
Merged

fix(operator-web): compose conversions through the storage version#1194
kimpenhaus merged 3 commits into
dotnet:mainfrom
alethic:fix/conversion-webhook-multi-hop

Conversation

@wasabii

@wasabii wasabii commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

Fixes #1193.

The conversion webhook only performed a single-hop conversion: ConversionWebhook<T>.Convert required a direct registered converter edge from an object's stored apiVersion to the request's desiredAPIVersion, and silently skipped any object it couldn't convert in one step.

Converters are registered hub-and-spoke — every served version converts to and from the storage version, exactly as the docs prescribe. But the API server legitimately asks the webhook to convert between two non-storage versions: this happens whenever an object remains persisted under a version that is no longer the storage version (so .status.storedVersions contains more than the current storage version) and is then read at a third served version. There is no direct edge for that pair, so the object was dropped and the read failed with:

conversion webhook for <group>/<version>, Kind=<Kind> returned 0 objects, expected 1

This makes the affected resource unreadable at some versions (kubectl, informers, GC, Helm/Flux) until every stored object is migrated to the current storage version.

How

Convert now composes the conversion through intermediate versions using a breadth-first search over the registered converter graph, so a A → storage → B chain is used when there is no direct A → B converter. A hub-and-spoke converter set is therefore sufficient for 3+ served versions, and the object is no longer silently dropped.

A direct converter, when one exists, is found as the single-hop shortest path — so existing behaviour is unchanged.

Tests

Added ConversionWebhookTest (5 cases) covering the hub-and-spoke set from the docs example:

  • composition between two non-storage versions (both directions) — the regression;
  • direct hops to/from the storage version still work;
  • an object with an unroutable source version is still skipped.

dotnet format --verify-no-changes and the full KubeOps.Operator.Web.Test suite pass.

🤖 Generated with Claude Code

The conversion webhook only performed a single-hop conversion: it required a
direct converter edge from an object's stored apiVersion to the requested
desiredAPIVersion. Converters are registered hub-and-spoke (every served
version converts to and from the storage version), so the API server's request
to convert between two non-storage versions -- which happens whenever an object
is persisted under a version that is no longer the storage version and read at a
third served version -- had no direct edge and the object was silently dropped,
surfacing as "conversion webhook ... returned 0 objects, expected N".

Compose the conversion through intermediate versions with a breadth-first search
over the registered converter graph, so a hub-and-spoke converter set is
sufficient for 3+ served versions. A direct converter, when present, is still
selected as the single-hop shortest path, so existing behaviour is unchanged.

Closes dotnet#1193

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 12, 2026 00:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes conversion webhook behavior for CRDs with 3+ served versions by allowing multi-hop conversions (e.g., v2 → storage → v1) instead of requiring a direct registered converter edge, addressing unreadable resources when objects remain persisted in older stored versions.

Changes:

  • Updated ConversionWebhook<TEntity>.Convert to build and apply a shortest-path conversion chain (BFS) across registered converter edges.
  • Added unit tests covering composed conversions between non-storage versions, direct hops to/from storage, and unroutable source versions.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionWebhook.cs Implements BFS-based conversion chaining across registered converter graph.
test/KubeOps.Operator.Web.Test/Webhooks/Conversion/ConversionWebhook.Test.cs Adds regression tests for composed conversions and existing direct-hop behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionWebhook.cs
@wasabii

wasabii commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

@dotnet-policy-service agree

wasabii and others added 2 commits July 11, 2026 19:40
… a request

The conversion chain depends only on the source and desired apiVersions, and the
desired version is constant for a request, so the chain is now memoised by source
apiVersion. A LIST response typically converts many objects sharing the same
stored version; this avoids repeating the breadth-first graph search (and the
scans over the conversions list) for every object. Unroutable source versions are
cached as null so they are searched only once as well.

Adds a batch test covering a repeated source version and an unroutable one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject conversion requests when an object has no valid apiVersion or no conversion path instead of returning an incomplete object list. Add ConversionWebhook test traits and coverage for invalid objects and partial batch responses.
@kimpenhaus

kimpenhaus commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

@wasabii I added a follow-up commit that improves conversion error handling:

  • rejects the complete ConversionReview when an object has no valid apiVersion or no conversion path (based on the kubernetes docs);
  • avoids returning partial convertedObjects lists;

I could not push directly to the fork branch.
The commit is available here:

5f7acbc

It can be applied with:

  git fetch https://github.com/dotnet/dotnet-operator-sdk.git pr-1194
  git cherry-pick 5f7acbc
  git push

Thanks 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug]: Conversion webhook only does single-hop conversion — fails (returned 0 objects) converting between two non-storage versions

3 participants