Skip to content

Conversation

@JoelSpeed
Copy link
Contributor

@JoelSpeed JoelSpeed commented Feb 10, 2026

This is currently stacked on #457, so only the last 10 commits are relevant to this PR.

This bootstraps the controller and logic for the compatibility requirement. Checking compatibility of the schema in the spec and updating the status appropriately.

Also implements the validating webhook for the CompatibilityRequirement to validate that the CRD data is valid, and that the fields we couldn't validate with openapi are validated (matchConditions, namespace selector, object selector)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added CRD compatibility validation framework with webhook-based enforcement to check CRD compatibility requirements.
    • Introduced controller reconciliation for managing and validating CRD compatibility constraints.
  • Tests

    • Added comprehensive test suite for CRD compatibility validation and controller workflows.
  • Chores

    • Updated multiple module dependencies to latest compatible versions.

@openshift-ci-robot
Copy link

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 10, 2026
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 10, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a new CRD compatibility checker controller for OpenShift that validates CustomResourceDefinitions against compatibility requirements via webhook-based admission control. It includes controller reconciliation logic, webhook validation, status management, supporting utilities, and comprehensive test coverage. Dependency versions are updated across multiple go.mod files to align with Kubernetes 1.34.1.

Changes

Cohort / File(s) Summary
Application Setup
cmd/crd-compatibility-checker/main.go
Integrates CompatibilityRequirementReconciler initialization and SetupWithManager into the operator's main manager lifecycle with error handling.
Kubernetes Manifests
manifests/0000_20_crd-compatibility-checker_06_webhooks.yaml
Adds ValidatingWebhookConfiguration for CompatibilityRequirement CRD validation, routing to webhook service with v1 admissionReview and fail-closed policy.
CRD Compatibility Controller
pkg/controllers/crdcompatibility/controller.go, reconcile.go, crdcompatibility_webhook.go, status.go, finalizer.go, index/crdcompatibilityrequirement.go
Core controller implementation with reconciliation entry point, webhook validator for CRD YAML parsing and validation, status condition management, finalizer lifecycle, and indexed lookups by CRD name.
CRD Checker
pkg/crdchecker/crdchecker.go, servedversionremoval.go
Compatibility checking logic that validates CRD modifications, detects removed served versions, and aggregates validation errors/warnings.
Controller Tests
pkg/controllers/crdcompatibility/controller_test.go, reconcile_test.go, suite_test.go, util_test.go
Test suites for reconciler setup, reconciliation flows, status conditions, webhook denial scenarios, CRD lifecycle, and test utilities for object creation/cleanup.
CRD Checker Tests
pkg/crdchecker/crdchecker_test.go
Test coverage for compatibility validation scenarios including field removal, version handling, and error conditions.
Test Utilities
pkg/test/conditions.go, crdbuilder.go, matchers.go
Gomega matchers for condition assertions, CRD/CompatibilityRequirement test generators, and Kubernetes error detection helpers.
General Utilities
pkg/util/map.go, norequeue.go, sync.go
Generic slice mapping, no-requeue error handling for configuration errors, and condition LastTransitionTime synchronization.
Dependency Updates
go.mod, e2e/go.mod, hack/tools/go.mod, manifests-gen/go.mod
Kubernetes module replacements pinning to v0.34.1+, kustomize bump to v5.7.1, and indirect dependency version alignments (yaml, json-patch, kube-openapi, kustomize/config).

Sequence Diagram

sequenceDiagram
    participant Client as Webhook Client
    participant Webhook as ValidatingWebhook
    participant Reconciler as CompatibilityReconciler
    participant Checker as CRDChecker
    participant Status as StatusWriter

    Client->>Webhook: POST /validate (CompatibilityRequirement)
    Webhook->>Webhook: Parse embedded CRD YAML
    Webhook->>Webhook: Validate CRD structure
    Webhook->>Webhook: Build/Validate WebhookConfig
    Webhook-->>Client: admission response (allow/deny)

    Note over Reconciler: Reconciliation triggered
    Reconciler->>Reconciler: Load CompatibilityRequirement
    Reconciler->>Reconciler: Set finalizer
    Reconciler->>Reconciler: Parse CRD from requirement
    Reconciler->>Reconciler: Fetch current live CRD
    Reconciler->>Checker: CheckCompatibilityRequirement
    Checker->>Checker: Build validation registry
    Checker->>Checker: Run compatibility checks
    Checker-->>Reconciler: errors, warnings
    Reconciler->>Status: Write status conditions
    Status->>Status: Set Admitted/Compatible/Progressing
    Status-->>Reconciler: status patched
    Reconciler-->>Reconciler: Return result/error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A controller hops to validate CRDs so fine,
Webhooks guard the schema, checking line by line.
With conditions and finalizers held with care,
Compatibility blooms through the operator's air! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main change: implementing a CompatibilityRequirement reconciler and webhook, with a clear reference to the Jira ticket.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 10, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign chrischdi for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@JoelSpeed JoelSpeed force-pushed the bootstrap-compatibility-reconciler branch from 1ce9e6e to 582b1d7 Compare February 12, 2026 14:52
@JoelSpeed JoelSpeed marked this pull request as ready for review February 12, 2026 15:03
@openshift-ci openshift-ci bot requested review from damdo and mdbooth February 12, 2026 15:03
@JoelSpeed JoelSpeed changed the title [WIP] Initial implementation of CompatibilityRequirement reconciler and webhook OCPCLOUD-3005: Initial implementation of CompatibilityRequirement reconciler and webhook Feb 12, 2026
@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Feb 12, 2026
@openshift-ci-robot
Copy link

openshift-ci-robot commented Feb 12, 2026

@JoelSpeed: This pull request references OCPCLOUD-3005 which is a valid jira issue.

Details

In response to this:

This is currently stacked on #457, so only the last 10 commits are relevant to this PR.

This bootstraps the controller and logic for the compatibility requirement. Checking compatibility of the schema in the spec and updating the status appropriately.

Also implements the validating webhook for the CompatibilityRequirement to validate that the CRD data is valid, and that the fields we couldn't validate with openapi are validated (matchConditions, namespace selector, object selector)

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 12, 2026
@JoelSpeed JoelSpeed force-pushed the bootstrap-compatibility-reconciler branch from 582b1d7 to 1015148 Compare February 12, 2026 15:46
@openshift-ci-robot
Copy link

openshift-ci-robot commented Feb 12, 2026

@JoelSpeed: This pull request references OCPCLOUD-3005 which is a valid jira issue.

Details

In response to this:

This is currently stacked on #457, so only the last 10 commits are relevant to this PR.

This bootstraps the controller and logic for the compatibility requirement. Checking compatibility of the schema in the spec and updating the status appropriately.

Also implements the validating webhook for the CompatibilityRequirement to validate that the CRD data is valid, and that the fields we couldn't validate with openapi are validated (matchConditions, namespace selector, object selector)

Summary by CodeRabbit

Release Notes

  • New Features

  • Added CRD compatibility validation framework with webhook-based enforcement to check CRD compatibility requirements.

  • Introduced controller reconciliation for managing and validating CRD compatibility constraints.

  • Tests

  • Added comprehensive test suite for CRD compatibility validation and controller workflows.

  • Chores

  • Updated multiple module dependencies to latest compatible versions.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go.mod (1)

60-71: ⚠️ Potential issue | 🟡 Minor

Review k8s.io/kubernetes version; v1.34.4 is available.

The versions are all confirmed releases (k8s.io/kubernetes v1.34.1 released Sept 10, 2025; sigs.k8s.io/crdify v0.5.0 released Aug 25, 2025). However, note that v1.34.4 was released on February 10, 2026. Consider updating to the latest patch unless v1.34.1 is pinned intentionally for this release train.

🤖 Fix all issues with AI agents
In `@manifests-gen/go.mod`:
- Line 21: The go.mod entry "go.yaml.in/yaml/v2 v2.4.3" is invalid; replace or
correct it to use the canonical module path and an existing tag (e.g., change
the module to "gopkg.in/yaml.v2" and the version to "v2.4.0"), or if a different
yaml library was intended, update the module path and version to the correct
upstream name and tag so the go.mod line for the yaml dependency is valid.

In `@pkg/controllers/crdcompatibility/finalizer.go`:
- Around line 39-42: clearFinalizer currently omits the finalizers field in the
server-side apply patch so SSA treats it as “do not modify” and the finalizer is
never removed; update the apply config passed to writeFinalizer (the
CompatibilityRequirement(...) used inside clearFinalizer) to explicitly set an
empty finalizers array (e.g. call WithFinalizers() with no args or
WithFinalizers([]string{})) so the SSA patch clears the finalizer, keeping
ForceOwnership behavior intact and allowing reconcileDelete to complete.

In `@pkg/controllers/crdcompatibility/status.go`:
- Around line 36-41: The log message in reconcileState.writeStatus is incorrect:
the condition (obj.DeletionTimestamp.IsZero() &&
!slices.Contains(obj.Finalizers, finalizerName)) detects an object that is NOT
being deleted but is missing the finalizer, so update the log emitted via
logf.FromContext(ctx).Info to accurately state that status write is being
skipped because the object lacks the finalizer (reference writeStatus,
obj.DeletionTimestamp.IsZero(), slices.Contains(..., finalizerName), and
logf.FromContext(ctx).Info) so the message matches the actual condition.

In `@pkg/crdchecker/crdchecker_test.go`:
- Around line 82-93: The test assertion is inverted and uses the wrong matcher
type: replace Expect(errors).NotTo(ConsistOf(MatchError("removed field :
v1.^.spec"))) with an assertion that checks the string slice contains the
expected message; e.g. use Expect(errors).To(ContainElement("removed field :
v1.^.spec")) or Expect(errors).To(ContainElement(MatchRegexp(`removed field :
v1\.\^\.spec`))). Update the same pattern in the other failing test (the similar
assertion around the second failing block) and locate the checks in the test
that call runTest and use getCRDVersion to modify the CRD schema.
- Around line 38-43: getCRDVersion currently returns &v from a range loop which
yields a copy; change it to iterate with index and return a pointer to the
actual slice element (e.g., &crd.Spec.Versions[i]) when v.Name == version,
otherwise return nil; update function getCRDVersion to use for i := range
crd.Spec.Versions { v := &crd.Spec.Versions[i]; if v.Name == version { return v
} } to ensure callers mutate the real element.

In `@pkg/util/sync.go`:
- Around line 60-61: The loop compares condition.Type to *conditionAC.Type and
can panic if conditionAC or conditionAC.Type is nil; add a guard before
dereferencing: ensure conditionAC != nil and conditionAC.Type != nil (or handle
nil Type explicitly) before using *conditionAC.Type, mirroring the nil checks
used in matchingCondition so the comparison is safe and consistent with the
existing defensive checks.
🧹 Nitpick comments (12)
pkg/util/norequeue.go (2)

35-39: Consider handling nil error to prevent potential panic.

If err is nil, calling Error() on the returned value will panic due to nil dereference on the embedded error interface.

🛡️ Optional defensive fix
 func NoRequeueError(err error, reason string) error {
+	if err == nil {
+		return nil
+	}
 	return &noRequeueError{err, reason}
 }

54-62: Unexported return type limits external usability.

AsNoRequeueError is exported but returns *noRequeueError (unexported). External callers cannot access the Reason field without reflection. If this is intended for internal use only, this is fine. Otherwise, consider exporting the type or adding a getter method.

♻️ Option 1: Export the type
-type noRequeueError struct {
+type NoRequeueError struct {
 	error
 	Reason string
 }

Then update the return type of AsNoRequeueError accordingly.

♻️ Option 2: Add a getter method
func (e *noRequeueError) GetReason() string {
	return e.Reason
}
go.mod (1)

340-344: Align controller-manager indirect version with other 0.34.1 pins.

The replace block pins k8s.io/controller-manager to v0.34.1 (line 24), but the indirect require shows v0.32.1 (line 341). While the replace directive will override this, aligning the require list prevents confusion and maintains consistency with other k8s.io components at v0.34.1.

Proposed alignment
-	k8s.io/controller-manager v0.32.1 // indirect
+	k8s.io/controller-manager v0.34.1 // indirect
pkg/util/map.go (1)

18-22: Consider distinguishing nil input from empty input behavior if this function may be JSON-serialized. Go's code review guidelines prefer nil slices as empty values, so returning nil for len(source) == 0 is idiomatic. However, if the caller JSON-marshals the result and needs [] instead of null, explicitly check source == nil to preserve that distinction.

♻️ Suggested change (if JSON serialization is a concern)
 func SliceMap[A, B any](source []A, fn func(A) B) []B {
-	if len(source) == 0 {
+	if source == nil {
 		return nil
 	}
 	result := make([]B, len(source))
 	for i, a := range source {
pkg/crdchecker/servedversionremoval.go (1)

80-85: Deterministic ordering for removed version list.

UnsortedList() can make error strings non-deterministic; sorting avoids flaky comparisons.

♻️ Suggested fix
 import (
 	"errors"
 	"fmt"
+	"sort"
 	"strings"
@@
 	removedVersions := oldServedVersions.Difference(newServedVersions)
@@
 	var err error
 	if len(removedVersions) > 0 {
-		err = fmt.Errorf("%w : %v", ErrRemovedServedVersions, strings.Join(removedVersions.UnsortedList(), ", "))
+		removed := removedVersions.UnsortedList()
+		sort.Strings(removed)
+		err = fmt.Errorf("%w : %s", ErrRemovedServedVersions, strings.Join(removed, ", "))
 	}
pkg/controllers/crdcompatibility/index/crdcompatibilityrequirement.go (1)

33-40: Consider handling empty CRDName in the indexer.

When Status.CRDName is empty, the indexer returns []string{""}, which indexes the object under an empty string key. This could cause unexpected matches when querying for objects with no CRD name set. Consider returning an empty slice to avoid indexing objects without a CRD name.

♻️ Suggested improvement
 func CRDByName(obj client.Object) []string {
 	requirement, ok := obj.(*apiextensionsv1alpha1.CompatibilityRequirement)
 	if !ok {
 		panic(fmt.Sprintf("Expected a CompatibilityRequirement but got a %T", obj))
 	}

+	if requirement.Status.CRDName == "" {
+		return nil
+	}
+
 	return []string{requirement.Status.CRDName}
 }
pkg/controllers/crdcompatibility/reconcile.go (2)

154-162: Sequential dependency handled via nil guards, but consider explicit sequencing for clarity.

Using errors.Join runs all three functions regardless of prior failures. While the nil guards in fetchCurrentCRD and checkCompatibilityRequirement prevent crashes, this pattern obscures the intended sequential dependency. Consider explicit sequencing if the functions should short-circuit on failure.

The current approach works due to nil guards, but explicit sequencing would make the dependencies clearer:

♻️ Alternative explicit sequencing
-	err := errors.Join(
-		r.parseCompatibilityCRD(obj),
-		r.fetchCurrentCRD(ctx, logger),
-		r.checkCompatibilityRequirement(),
-	)
-
-	if err != nil {
-		return ctrl.Result{}, err
-	}
+	if err := r.parseCompatibilityCRD(obj); err != nil {
+		return ctrl.Result{}, err
+	}
+
+	if err := r.fetchCurrentCRD(ctx, logger); err != nil {
+		return ctrl.Result{}, err
+	}
+
+	if err := r.checkCompatibilityRequirement(); err != nil {
+		return ctrl.Result{}, err
+	}

114-120: Remove unnecessary else after return.

The else block after the return nil statement on line 116 is unnecessary and can be simplified.

♻️ Suggested simplification
 	if err := r.client.Get(ctx, types.NamespacedName{Name: crdName}, currentCRD); err != nil {
 		if apierrors.IsNotFound(err) {
 			log.Info("CRD not found", "crdRef", crdName)
 			return nil
-		} else {
-			return fmt.Errorf("failed to fetch CRD %s: %w", crdName, err)
 		}
+		return fmt.Errorf("failed to fetch CRD %s: %w", crdName, err)
 	}
pkg/controllers/crdcompatibility/reconcile_test.go (1)

152-159: Consider extracting addProperty as a reusable test helper.

The addProperty helper is defined within the nested Context but could be useful in other tests. Consider moving it to the test helpers package if CRD schema modification is needed elsewhere.

pkg/controllers/crdcompatibility/crdcompatibility_webhook.go (1)

66-69: Unnecessary double pointer in yaml.Unmarshal.

On line 66, compatibilityCRD is declared as *apiextensionsv1.CustomResourceDefinition, then line 67 passes &compatibilityCRD (a **CustomResourceDefinition) to yaml.Unmarshal. While this works because the YAML library handles dereferencing, passing the pointer directly is more idiomatic.

♻️ Suggested simplification
 	// Parse the CRD in compatibilityCRD into a CRD object.
-	compatibilityCRD := &apiextensionsv1.CustomResourceDefinition{}
-	if err := yaml.Unmarshal([]byte(compatibilityRequirement.Spec.CompatibilitySchema.CustomResourceDefinition.Data), &compatibilityCRD); err != nil {
+	compatibilityCRD := &apiextensionsv1.CustomResourceDefinition{}
+	if err := yaml.Unmarshal([]byte(compatibilityRequirement.Spec.CompatibilitySchema.CustomResourceDefinition.Data), compatibilityCRD); err != nil {
 		return nil, fmt.Errorf("%w: %w", errInvalidCompatibilityCRD, err)
 	}
pkg/controllers/crdcompatibility/suite_test.go (1)

257-259: Consider logging unexpected Kind values for debugging.

The default case in the switch silently ignores unexpected webhook kinds. While this may be intentional for flexibility, logging a debug message could help diagnose issues when new webhook types are added to the manifest.

♻️ Suggested improvement
 			// Ignore unexpected kinds
 			default:
+				// Optionally log for debugging:
+				// GinkgoLogr.Info("Ignoring unexpected kind in webhook manifest", "kind", generic.Kind)
 			}
pkg/controllers/crdcompatibility/controller.go (1)

96-120: Consider returning a requeue to handle transient list failures.

When List fails (line 105-108), the function logs and returns nil, meaning the CRD change won't trigger any reconciliations. While this is a common pattern and the next CRD update will retry, transient failures could delay reconciliation indefinitely if the CRD doesn't change again.

This is acceptable for now since requirements are also reconciled on their own events, but worth noting for future consideration if tighter consistency guarantees are needed.

github.com/openshift/api v0.0.0-20260205045418-81371d13d1fc
github.com/openshift/cluster-capi-operator v0.0.0-00010101000000-000000000000
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd manifests-gen && head -80 go.mod | tail -60

Repository: openshift/cluster-capi-operator

Length of output: 2947


🏁 Script executed:

#!/bin/bash

# For k8s.io/utils - check if the commit exists
echo "=== Checking k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 ==="
# Extract commit hash from pseudo-version (last 12 chars)
curl -s "https://api.github.com/repos/kubernetes/utils/commits/bc988d571ff4" | jq -r '.sha // .message' | head -1

echo ""
echo "=== Checking k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ==="
curl -s "https://api.github.com/repos/kubernetes/kube-openapi/commits/589584f1c912" | jq -r '.sha // .message' | head -1

echo ""
echo "=== Checking sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 ==="
curl -s "https://api.github.com/repos/kubernetes-sigs/json/commits/2d320260d730" | jq -r '.sha // .message' | head -1

echo ""
echo "=== Checking gopkg.in/evanphx/json-patch.v4 v4.13.0 ==="
curl -s "https://api.github.com/repos/evanphx/json-patch/releases/tags/v4.13.0" | jq -r '.tag_name // .message' | head -1

echo ""
echo "=== Checking go.yaml.in/yaml/v2 v2.4.3 ==="
curl -s "https://api.github.com/repos/go-yaml/yaml/releases/tags/v2.4.3" | jq -r '.tag_name // .message' | head -1

Repository: openshift/cluster-capi-operator

Length of output: 1213


🌐 Web query:

k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 kubernetes go module

💡 Result:

k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 is a Go pseudo-version of the k8s.io/utils module (repo: github.com/kubernetes/utils) that pins the dependency to commit bc988d571ff4 dated Oct 2, 2025. [1][2]

To use it:

go get k8s.io/utils@v0.0.0-20251002143259-bc988d571ff4
# or equivalently
go get k8s.io/utils@bc988d571ff4

On pkg.go.dev, this version appears in the module’s version list; newer pseudo-versions also exist (e.g., v0.0.0-20260108192941-914a6e750570 dated Jan 8, 2026). [1][3]

Sources: [1] [2] [3]


🌐 Web query:

k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 kubernetes release

💡 Result:

That k8s.io/kube-openapi pseudo-version (v0.0.0-20250910181357-589584f1c912) corresponds to the Kubernetes v1.35.0 release line (i.e., it’s used by Kubernetes 1.35.x components). This shows up in the SBOM for kube-controller-manager v1.35.0, which lists k8s.io/kube-openapi at exactly that version. [1]

Kubernetes v1.35.0 release date: 2025-12-17. [2]

Sources: [1] [2]


🌐 Web query:

sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 kubernetes-sigs

💡 Result:

sigs.k8s.io/json is a Go module hosted in the Kubernetes SIGs GitHub org at github.com/kubernetes-sigs/json (the import path sigs.k8s.io/json resolves to that repo). [1]

v0.0.0-20250730193827-2d320260d730 is a Go pseudo-version, meaning it points to a specific commit (2d320260d730) with the embedded timestamp 2025-07-30 19:38:27 UTC. The module version is shown as published on July 30, 2025. [1][2]

The library provides JSON decoding helpers (case-sensitive key handling, integer-preserving decoding, and stricter decoding options) on top of Go’s encoding/json. [2][3]


🌐 Web query:

gopkg.in/evanphx/json-patch.v4 v4.13.0 release

💡 Result:

gopkg.in/evanphx/json-patch.v4 v4.13.0 is the latest v4 tag, published Jan 28, 2025. [1][2]

Release note / main change: Remove github.com/pkg/errors dependency. [3]

Install: go get gopkg.in/evanphx/json-patch.v4@v4.13.0 [2]

Sources:
[1] GitHub tags page (shows v4.13.0 date) (github.com)
[2] pkg.go.dev module page (shows v4.13.0 “Published: Jan 28, 2025”) (pkg.go.dev)
[3] GitHub release v4.13.0 (release note text) (github.com)

Citations:


🌐 Web query:

go.yaml.in/yaml/v2 v2.4.3 go-yaml package

💡 Result:

go.yaml.in/yaml/v2 doesn’t appear to be the canonical import path for the Go “go-yaml” library. The widely used go-yaml/yaml package uses:

  • v2 import path: gopkg.in/yaml.v2 (repo: github.com/go-yaml/yaml) [1]
  • Latest tagged v2 release: v2.4.0 (tagged Nov 17, 2020) — there is no v2.4.3 tag in the upstream repository [2][3]
  • Latest v3 tag shown: v3.0.1 (May 27, 2022) [2]

Pin to the latest upstream v2 like this:

go get gopkg.in/yaml.v2@v2.4.0

If you meant a different fork/module that really has v2.4.3, paste the module path (as it appears in go.mod) and I’ll look up that specific artifact.


Fix or justify go.yaml.in/yaml/v2 v2.4.3—version does not exist.

Four of the five bumped dependencies are valid:

  • ✅ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 (pseudo-version, Oct 2, 2025)
  • ✅ k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 (used in Kubernetes v1.35.0)
  • ✅ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 (pseudo-version, Jul 30, 2025)
  • ✅ gopkg.in/evanphx/json-patch.v4 v4.13.0 (released Jan 28, 2025)

However, go.yaml.in/yaml/v2 v2.4.3 is invalid. The canonical go-yaml package uses import path gopkg.in/yaml.v2, and the latest v2 release is v2.4.0 (Nov 2020)—no v2.4.3 tag exists. Verify the intended module path and version, or correct to gopkg.in/yaml.v2@v2.4.0.

🤖 Prompt for AI Agents
In `@manifests-gen/go.mod` at line 21, The go.mod entry "go.yaml.in/yaml/v2
v2.4.3" is invalid; replace or correct it to use the canonical module path and
an existing tag (e.g., change the module to "gopkg.in/yaml.v2" and the version
to "v2.4.0"), or if a different yaml library was intended, update the module
path and version to the correct upstream name and tag so the go.mod line for the
yaml dependency is valid.

Comment on lines +39 to +42
func clearFinalizer(ctx context.Context, cl client.Client, obj *apiextensionsv1alpha1.CompatibilityRequirement) error {
return writeFinalizer(ctx, cl, obj, apiextensionsapplyconfig.CompatibilityRequirement(obj.GetName()).
WithUID(obj.GetUID()))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual implementation of clearFinalizer and related code
cat -n pkg/controllers/crdcompatibility/finalizer.go | head -60

Repository: openshift/cluster-capi-operator

Length of output: 2341


🏁 Script executed:

# Find where WithFinalizers is defined
fd -e go | xargs grep -l "func.*WithFinalizers" | head -5

Repository: openshift/cluster-capi-operator

Length of output: 487


🏁 Script executed:

# Search for how apply configs handle finalizers in this codebase
rg "WithFinalizers" -A 3 -B 3 --type go

Repository: openshift/cluster-capi-operator

Length of output: 50388


🏁 Script executed:

# Check the writeFinalizer function to understand the full context
rg "writeFinalizer" -A 10 -B 3 --type go

Repository: openshift/cluster-capi-operator

Length of output: 1945


🏁 Script executed:

# Look for tests of the finalizer functions
fd -e go | xargs grep -l "clearFinalizer\|setFinalizer" | head -10

Repository: openshift/cluster-capi-operator

Length of output: 166


🏁 Script executed:

# Check if there are any tests in the crdcompatibility package
find pkg/controllers/crdcompatibility -name "*test*" -type f

Repository: openshift/cluster-capi-operator

Length of output: 270


🏁 Script executed:

# Look for documentation or comments about SSA and finalizer removal
rg "ApplyConfigPatch|ForceOwnership" -B 5 -A 5 --type go | head -100

Repository: openshift/cluster-capi-operator

Length of output: 5732


🏁 Script executed:

# Search for how to properly remove finalizers in the codebase
rg "finalizer" -i --type go -A 3 -B 3 | grep -E "(clear|remove|delete)" -A 3 -B 3 | head -50

Repository: openshift/cluster-capi-operator

Length of output: 5215


🏁 Script executed:

# Look at the test files for finalizer testing
cat -n pkg/controllers/crdcompatibility/controller_test.go | head -100

Repository: openshift/cluster-capi-operator

Length of output: 4356


🏁 Script executed:

# Search for tests related to finalizer removal
rg "clearFinalizer|setFinalizer" -A 10 -B 5 pkg/controllers/crdcompatibility

Repository: openshift/cluster-capi-operator

Length of output: 4076


🏁 Script executed:

# Check how the reconcile function uses these finalizer helpers
cat -n pkg/controllers/crdcompatibility/reconcile.go

Repository: openshift/cluster-capi-operator

Length of output: 6734


🏁 Script executed:

# Search for how other controllers properly handle finalizer removal
rg "finalizer" --type go -B 3 -A 3 | grep -E "(WithFinalizers|RemoveFinalizer)" -B 5 -A 5 | head -80

Repository: openshift/cluster-capi-operator

Length of output: 7610


clearFinalizer does not remove the finalizer via SSA as currently implemented.

With SSA, omitting the finalizers field from the patch means "don't modify this field." The ForceOwnership option takes over field ownership but does not affect fields absent from the patch. To explicitly remove finalizers owned by this field owner, you must include the field in the patch—either by calling WithFinalizers() with no arguments (to create an empty finalizers array) or by passing an explicit empty slice.

The current implementation will not actually clear the finalizer, leaving the object unable to complete deletion. This breaks the deletion reconciliation flow in reconcileDelete (line 172 of reconcile.go), which relies on clearFinalizer to remove the finalizer before the object can be garbage-collected.

🤖 Prompt for AI Agents
In `@pkg/controllers/crdcompatibility/finalizer.go` around lines 39 - 42,
clearFinalizer currently omits the finalizers field in the server-side apply
patch so SSA treats it as “do not modify” and the finalizer is never removed;
update the apply config passed to writeFinalizer (the
CompatibilityRequirement(...) used inside clearFinalizer) to explicitly set an
empty finalizers array (e.g. call WithFinalizers() with no args or
WithFinalizers([]string{})) so the SSA patch clears the finalizer, keeping
ForceOwnership behavior intact and allowing reconcileDelete to complete.

Comment on lines +36 to +41
func (r *reconcileState) writeStatus(ctx context.Context, obj *apiextensionsv1alpha1.CompatibilityRequirement, reconcileErr error) error {
// Don't write status if the object has no finalizer
if obj.DeletionTimestamp.IsZero() && !slices.Contains(obj.Finalizers, finalizerName) {
logf.FromContext(ctx).Info("Skipping status because the object is being deleted")
return nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Log message is misleading - the condition checks for missing finalizer, not deletion.

The condition obj.DeletionTimestamp.IsZero() && !slices.Contains(obj.Finalizers, finalizerName) evaluates to true when the object is not being deleted and lacks the finalizer. However, the log message states "Skipping status because the object is being deleted," which is inaccurate.

✏️ Suggested fix
 func (r *reconcileState) writeStatus(ctx context.Context, obj *apiextensionsv1alpha1.CompatibilityRequirement, reconcileErr error) error {
 	// Don't write status if the object has no finalizer
 	if obj.DeletionTimestamp.IsZero() && !slices.Contains(obj.Finalizers, finalizerName) {
-		logf.FromContext(ctx).Info("Skipping status because the object is being deleted")
+		logf.FromContext(ctx).Info("Skipping status because the object has no finalizer yet")
 		return nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (r *reconcileState) writeStatus(ctx context.Context, obj *apiextensionsv1alpha1.CompatibilityRequirement, reconcileErr error) error {
// Don't write status if the object has no finalizer
if obj.DeletionTimestamp.IsZero() && !slices.Contains(obj.Finalizers, finalizerName) {
logf.FromContext(ctx).Info("Skipping status because the object is being deleted")
return nil
}
func (r *reconcileState) writeStatus(ctx context.Context, obj *apiextensionsv1alpha1.CompatibilityRequirement, reconcileErr error) error {
// Don't write status if the object has no finalizer
if obj.DeletionTimestamp.IsZero() && !slices.Contains(obj.Finalizers, finalizerName) {
logf.FromContext(ctx).Info("Skipping status because the object has no finalizer yet")
return nil
}
🤖 Prompt for AI Agents
In `@pkg/controllers/crdcompatibility/status.go` around lines 36 - 41, The log
message in reconcileState.writeStatus is incorrect: the condition
(obj.DeletionTimestamp.IsZero() && !slices.Contains(obj.Finalizers,
finalizerName)) detects an object that is NOT being deleted but is missing the
finalizer, so update the log emitted via logf.FromContext(ctx).Info to
accurately state that status write is being skipped because the object lacks the
finalizer (reference writeStatus, obj.DeletionTimestamp.IsZero(),
slices.Contains(..., finalizerName), and logf.FromContext(ctx).Info) so the
message matches the actual condition.

Comment on lines +38 to +43
func getCRDVersion(crd *apiextensionsv1.CustomResourceDefinition, version string) *apiextensionsv1.CustomResourceDefinitionVersion {
for _, v := range crd.Spec.Versions {
if v.Name == version {
return &v
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "crdchecker_test.go" -o -name "crdchecker.go" | head -20

Repository: openshift/cluster-capi-operator

Length of output: 141


🏁 Script executed:

cd $(git ls-files | grep -E "crdchecker\.(go|test\.go)$" | head -1 | xargs dirname) 2>/dev/null && pwd || echo "not found"

Repository: openshift/cluster-capi-operator

Length of output: 108


🏁 Script executed:

git ls-files | grep crdchecker

Repository: openshift/cluster-capi-operator

Length of output: 176


🏁 Script executed:

cat -n pkg/crdchecker/crdchecker.go | head -80

Repository: openshift/cluster-capi-operator

Length of output: 2556


🏁 Script executed:

cat -n pkg/crdchecker/crdchecker_test.go | head -100

Repository: openshift/cluster-capi-operator

Length of output: 3864


🏁 Script executed:

grep -n "getCRDVersion" pkg/crdchecker/crdchecker_test.go

Repository: openshift/cluster-capi-operator

Length of output: 301


🏁 Script executed:

cat -n pkg/crdchecker/crdchecker_test.go | sed -n '82,110p'

Repository: openshift/cluster-capi-operator

Length of output: 1213


Fix getCRDVersion to return pointer to actual slice element.

In Go range loops, the loop variable v is a copy on the stack. Returning &v points to this stack copy, not the original element in crd.Spec.Versions. This causes mutations in test cases (lines 85-86 and 98-101) to not take effect, making the tests ineffective.

Suggested fix
 func getCRDVersion(crd *apiextensionsv1.CustomResourceDefinition, version string) *apiextensionsv1.CustomResourceDefinitionVersion {
-	for _, v := range crd.Spec.Versions {
-		if v.Name == version {
-			return &v
-		}
-	}
+	for i := range crd.Spec.Versions {
+		if crd.Spec.Versions[i].Name == version {
+			return &crd.Spec.Versions[i]
+		}
+	}
 
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func getCRDVersion(crd *apiextensionsv1.CustomResourceDefinition, version string) *apiextensionsv1.CustomResourceDefinitionVersion {
for _, v := range crd.Spec.Versions {
if v.Name == version {
return &v
}
}
func getCRDVersion(crd *apiextensionsv1.CustomResourceDefinition, version string) *apiextensionsv1.CustomResourceDefinitionVersion {
for i := range crd.Spec.Versions {
if crd.Spec.Versions[i].Name == version {
return &crd.Spec.Versions[i]
}
}
return nil
}
🤖 Prompt for AI Agents
In `@pkg/crdchecker/crdchecker_test.go` around lines 38 - 43, getCRDVersion
currently returns &v from a range loop which yields a copy; change it to iterate
with index and return a pointer to the actual slice element (e.g.,
&crd.Spec.Versions[i]) when v.Name == version, otherwise return nil; update
function getCRDVersion to use for i := range crd.Spec.Versions { v :=
&crd.Spec.Versions[i]; if v.Name == version { return v } } to ensure callers
mutate the real element.

Comment on lines +82 to +93
It("should fail when a field is removed", func() {
errors, _ := runTest(
func(target *apiextensionsv1.CustomResourceDefinition) *apiextensionsv1.CustomResourceDefinition {
version := getCRDVersion(target, "v1")
delete(version.Schema.OpenAPIV3Schema.Properties, "spec")

return target
},
)

Expect(errors).NotTo(ConsistOf(MatchError("removed field : v1.^.spec")), "should contain an error")
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix assertions: inverted expectation and wrong matcher type.

errors is []string, but MatchError expects error, and NotTo negates the intended check.

✅ Suggested fix
-			Expect(errors).NotTo(ConsistOf(MatchError("removed field : v1.^.spec")), "should contain an error")
+			Expect(errors).To(ContainElement(ContainSubstring("removed field : v1.^.spec")), "should contain an error")
@@
-			Expect(errors).NotTo(ConsistOf(MatchError("served versions removed : v1beta1")), "should contain an error")
+			Expect(errors).To(ContainElement(ContainSubstring("served versions removed : v1beta1")), "should contain an error")

Also applies to: 111-120

🤖 Prompt for AI Agents
In `@pkg/crdchecker/crdchecker_test.go` around lines 82 - 93, The test assertion
is inverted and uses the wrong matcher type: replace
Expect(errors).NotTo(ConsistOf(MatchError("removed field : v1.^.spec"))) with an
assertion that checks the string slice contains the expected message; e.g. use
Expect(errors).To(ContainElement("removed field : v1.^.spec")) or
Expect(errors).To(ContainElement(MatchRegexp(`removed field : v1\.\^\.spec`))).
Update the same pattern in the other failing test (the similar assertion around
the second failing block) and locate the checks in the test that call runTest
and use getCRDVersion to modify the CRD schema.

Comment on lines +60 to +61
for _, condition := range currentConditions {
if condition.Type == *conditionAC.Type {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential nil pointer dereference on conditionAC.Type.

Line 61 dereferences *conditionAC.Type without a nil check, while the matchingCondition closure defensively guards against nil for Status, Reason, Message, and ObservedGeneration. If Type is nil, this will panic.

Consider adding a guard clause for consistency with the defensive approach in matchingCondition:

🛡️ Proposed fix
 func SetLastTransitionTimeMetaV1(now metav1.Time, currentConditions []metav1.Condition, conditionAC *metav1applyconfig.ConditionApplyConfiguration) *metav1applyconfig.ConditionApplyConfiguration {
+	if conditionAC.Type == nil {
+		return conditionAC.WithLastTransitionTime(now)
+	}
+
 	matchingCondition := func(condition *metav1.Condition, conditionAC *metav1applyconfig.ConditionApplyConfiguration) bool {
🤖 Prompt for AI Agents
In `@pkg/util/sync.go` around lines 60 - 61, The loop compares condition.Type to
*conditionAC.Type and can panic if conditionAC or conditionAC.Type is nil; add a
guard before dereferencing: ensure conditionAC != nil and conditionAC.Type !=
nil (or handle nil Type explicitly) before using *conditionAC.Type, mirroring
the nil checks used in matchingCondition so the comparison is safe and
consistent with the existing defensive checks.

@JoelSpeed
Copy link
Contributor Author

/test e2e-aws-capi-techpreview
/test e2e-aws-ovn

Quick sample E2E

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Feb 12, 2026

@JoelSpeed: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-aws-capi-techpreview 1015148 link true /test e2e-aws-capi-techpreview

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

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

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants