Skip to content

OCPBUGS-62177: verify cert revocation against all KAS pods - #8263

Merged
openshift-merge-bot[bot] merged 3 commits into
openshift:mainfrom
sdminonne:OCPBUGS-62177
May 11, 2026
Merged

OCPBUGS-62177: verify cert revocation against all KAS pods#8263
openshift-merge-bot[bot] merged 3 commits into
openshift:mainfrom
sdminonne:OCPBUGS-62177

Conversation

@sdminonne

@sdminonne sdminonne commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

Updates the CertificateRevocationController to verify certificate trust and revocation against every individual KAS pod rather than through the service load balancer. In HA deployments with 3 KAS replicas, the service endpoint load-balances to a single pod, so the check could hit a pod that had loaded the updated trust bundle while others hadn't, causing premature state transitions in the revocation flow.

Changes

control-plane-pki-operator (CertificateRevocationController):

  • Add verifyCertificateAgainstAllKASPods which enumerates ready KAS pods via label selector and connects to each pod IP directly (with SNI set to the KAS service name for TLS validation)
  • Cross-check ready pod count against the KAS Deployment's expected replica count to prevent verifying only a subset during informer cache lag or pod restarts
  • Add dual-cert cross-checks: in ensureNewSignerCertificatePropagated, verify the old signer is also still trusted (detects mid-reload); in ensureOldSignerCertificateRevoked, verify the new signer cert is still trusted before declaring the old one revoked, preventing false positives when a pod restart temporarily rejects all certs
  • Wire a KAS pod informer so pod readiness changes trigger immediate reconciliation
  • Extract verifyCertificateTrusted and verifyCertificateRevoked helpers for per-pod SelfSubjectReview checks

control-plane-operator (RBAC):

  • Grant list/watch on pods and get on deployments to the control-plane-pki-operator role so it can enumerate KAS pods and verify replica counts (update role asset + 5 test fixtures)

support/podspec:

  • Add IsPodReady and ContainerPort helpers for pod readiness checks and named port lookup

e2e / integration:

  • Add TestCreateClusterHABreakGlassCredentials exercising the break-glass credential flow on a HighlyAvailable control plane (3 KAS replicas)
  • Add per-request timeout to revocation SSR polling in the integration test to prevent indefinite hangs when KAS stalls the TLS handshake during trust bundle reload
  • Fix hasWorkerNodes detection for private clusters by checking NodePool replica counts instead of assuming workers exist

Context

This was previously fixed in PRs #7405 + #7744 but reverted in #7784 because the e2e test added alongside it had a ~50% flake rate. The controller fix itself was sound — the flakiness was in the e2e test wiring. This PR re-applies the controller logic with comprehensive unit tests and a different, more targeted HA e2e test.

Which issue(s) this PR fixes:

Fixes https://issues.redhat.com/browse/OCPBUGS-62177

Special notes for your reviewer:

The dual-cert cross-checks are the key addition beyond the original fix. In ensureOldSignerCertificateRevoked, verifying the new cert is trusted before declaring the old one revoked prevents false positives when a KAS pod restart during revocation causes the old cert to become unreachable (connection refused), which looks like revocation but is actually the pod being down. Similarly, in ensureNewSignerCertificatePropagated, verifying the old cert is also still trusted prevents declaring propagation complete when a pod is mid-reload and happens to accept the new cert but hasn't finished loading the full trust bundle.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.
  • This change includes e2e.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

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 do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. do-not-merge/needs-area labels Apr 16, 2026
@openshift-ci

openshift-ci Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

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

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR changes the certificate revocation controller to verify API server certificates by connecting to each non-terminating, ready KAS pod instead of a single service endpoint. It lists pods labeled app=kube-apiserver from a pod informer, filters by termination/ready/PodIP, resolves the kube-apiserver container port by name with a default fallback, and performs an HTTPS SelfSubjectReview request to https://<PodIP>:<port> with TLS SNI set to the KAS service name. The controller now requires all ready pods to accept the new signer or reject the old signer before progressing. Helpers isPodReady, containerPort, verifyCertificateAgainstAllKASPods, and pod-informer plumbing were added. The RBAC Role was expanded to allow list and watch on core pods. Tests covering pod readiness, port lookup, and verification logic were added.

Sequence Diagram(s)

sequenceDiagram
    participant Controller as CertificateRevocationController
    participant Informer as Pod Informer / Cache
    participant Pod as KAS Pod
    participant K8sAPI as Kubernetes API Server

    Controller->>Informer: list pods (label=app=kube-apiserver)
    Informer-->>Controller: pod list
    Controller->>Controller: filter non-terminating, check isPodReady & PodIP
    alt no ready pods
        Controller->>Controller: requeue
    else ready pods exist
        loop for each ready pod
            Controller->>Controller: resolve container port (named or default)
            Controller->>Pod: HTTPS SSR to https://<PodIP>:<port> (TLS SNI = KAS service)
            Pod->>K8sAPI: forward SSR to kube-apiserver instance
            K8sAPI-->>Pod: SSR response (authorized/unauthorized)
            Pod-->>Controller: SSR result
        end
        Controller->>Controller: require all pods to accept/reject as gate condition
    end
    Controller->>K8sAPI: update certificate revocation status
Loading
🚥 Pre-merge checks | ✅ 10 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Structure And Quality ❓ Inconclusive The custom check requests evaluation of Ginkgo test code patterns (BeforeEach/AfterEach, Eventually/Consistently), but the test framework type is unclear from the repository context. The referenced test file is inaccessible, making assessment impossible. Clarify whether tests use Ginkgo BDD framework or standard Go testing, then re-evaluate with appropriate framework-specific criteria.
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: updating the certificate revocation controller to verify against all KAS pods instead of a single endpoint, which is the core architectural improvement in this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed Test file uses standard Go testing with t.Run() subtests and gomega assertions, not Ginkgo's BDD-style DSL. Check not applicable as no dynamic values embedded in stable test names.
Microshift Test Compatibility ✅ Passed This PR adds only standard Go unit tests for certificaterevocationcontroller, not Ginkgo e2e tests. The custom check targets only Ginkgo e2e tests (It(), Describe(), Context(), When() patterns), which are not present in this PR.
Single Node Openshift (Sno) Test Compatibility ✅ Passed This pull request does not add any Ginkgo e2e tests; it only contains RBAC updates and standard Go unit tests.
Topology-Aware Scheduling Compatibility ✅ Passed The PR introduces no topology-incompatible scheduling constraints. The controller uses per-pod certificate verification with label-based pod listing and direct IP connections, functioning correctly across all OpenShift topologies (SNO, TNF, TNA, HA, HyperShift).
Ote Binary Stdout Contract ✅ Passed PR modifies control-plane-pki-operator code and unit tests using standard Go testing patterns, not OTE Binary Stdout Contract.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed This PR does not add new Ginkgo e2e tests, only RBAC updates, controller logic modifications, and standard Go unit tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@openshift-ci openshift-ci Bot added area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/control-plane-pki-operator Indicates the PR includes changes for the control plane PKI operator - in an OCP release and removed do-not-merge/needs-area labels Apr 16, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go`:
- Around line 1320-1349: The test currently only asserts the aggregate boolean
result for verifyCertificateAgainstAllKASPods; update the case so the testFunc
used in the scenario records/counts invocations (e.g., increment a counter
closed over by the test) and assert that the counter equals the number of ready,
non-terminating pods (two in this case) in addition to checking expectedResult;
modify the test case's testFunc and add an assertion after calling
verifyCertificateAgainstAllKASPods to enforce one callback invocation per ready
pod.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`:
- Around line 603-612: The code currently forces pod-level TLS verification off
by setting podCfg.TLSClientConfig.Insecure = true and clearing CAData/CAFile;
instead remove that line and restore CA verification by copying the CA from the
original admin config into the pod config (e.g., set
podCfg.TLSClientConfig.CAData = adminCfg.TLSClientConfig.CAData and
podCfg.TLSClientConfig.CAFile = adminCfg.TLSClientConfig.CAFile) while keeping
podCfg.TLSClientConfig.ServerName = hcpmanifests.KubeAPIServerServiceName so
SNI/hostname verification works for the pod connection in
certificaterevocationcontroller (use the podCfg and adminCfg symbols to locate
the change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b0040c3-97c3-458f-a2ee-f91dc0478343

📥 Commits

Reviewing files that changed from the base of the PR and between 846f2e9 and 7785913.

⛔ Files ignored due to path filters (5)
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/AROSwift/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/GCP/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/IBMCloud/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
📒 Files selected for processing (3)
  • control-plane-operator/controllers/hostedcontrolplane/v2/assets/control-plane-pki-operator/role.yaml
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go

@codecov

codecov Bot commented Apr 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.19802% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.64%. Comparing base (bded456) to head (7c7f3ce).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
...ationcontroller/certificaterevocationcontroller.go 78.49% 30 Missing and 10 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8263      +/-   ##
==========================================
+ Coverage   37.53%   37.64%   +0.10%     
==========================================
  Files         751      751              
  Lines       92026    92160     +134     
==========================================
+ Hits        34544    34691     +147     
+ Misses      54841    54812      -29     
- Partials     2641     2657      +16     
Files with missing lines Coverage Δ
support/podspec/containers.go 42.26% <100.00%> (+6.07%) ⬆️
...ationcontroller/certificaterevocationcontroller.go 55.55% <78.49%> (+8.46%) ⬆️
Flag Coverage Δ
cmd-support 32.80% <100.00%> (+0.03%) ⬆️
cpo-hostedcontrolplane 36.77% <ø> (ø)
cpo-other 37.76% <ø> (ø)
hypershift-operator 47.93% <ø> (ø)
other 28.80% <78.49%> (+1.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go (1)

99-100: Wire the Pods informer into the controller dependency chain.

Pod readiness and IP now gate revocation progress. Calling Pods().Informer() for a side effect (lines 99-100) does not register the informer with the controller factory, so the pod cache won't participate in initial sync and pod transitions won't trigger re-enqueuing of CRR work. Pass the informer to the controller factory to make pod updates drive reconciliation immediately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`
around lines 99 - 100, The Pods informer is only being created for side effects
(kubeInformersForNamespaces.InformersFor(hostedControlPlane.Namespace).Core().V1().Pods().Informer())
but not registered with the controller factory, so pod events won't drive
reconciliation; change the code to capture the informer (e.g., podsInformer :=
kubeInformersForNamespaces.InformersFor(hostedControlPlane.Namespace).Core().V1().Pods().Informer())
and pass that informer into the controller factory/constructor used to build the
CertificateRevocation controller (the controller factory or
NewController/WithInformer call you use to wire controllers) so the pod cache
participates in initial sync and pod updates enqueue CRR work immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`:
- Around line 603-617: The per-pod client probes currently inherit adminCfg with
no timeout, so create a bounded timeout for each pod: set podCfg.Timeout =
<reasonable duration> (e.g., 10s) after building podCfg (the struct from
rest.AnonymousClientConfig) to ensure the generated http client has a request
timeout, and also wrap the testFunc call with a child context with the same
timeout (use context.WithTimeout(ctx, <same duration>) and defer cancel) before
calling testFunc(ctxWithTimeout, podClient) to avoid hanging reconcile workers;
update references around podCfg, podClient, and testFunc accordingly.

---

Nitpick comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`:
- Around line 99-100: The Pods informer is only being created for side effects
(kubeInformersForNamespaces.InformersFor(hostedControlPlane.Namespace).Core().V1().Pods().Informer())
but not registered with the controller factory, so pod events won't drive
reconciliation; change the code to capture the informer (e.g., podsInformer :=
kubeInformersForNamespaces.InformersFor(hostedControlPlane.Namespace).Core().V1().Pods().Informer())
and pass that informer into the controller factory/constructor used to build the
CertificateRevocation controller (the controller factory or
NewController/WithInformer call you use to wire controllers) so the pod cache
participates in initial sync and pod updates enqueue CRR work immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: e75e2dca-ee10-4fe8-951e-575e5dcc6fc0

📥 Commits

Reviewing files that changed from the base of the PR and between 7785913 and c345009.

📒 Files selected for processing (1)
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go (2)

1376-1378: Strengthen the listPods mock contract.

Line 1376 currently ignores namespace and selector, so selector/namespace regressions won’t be caught by this test.

Suggested diff
 		t.Run(testCase.name, func(t *testing.T) {
+			expectedSelector := labels.SelectorFromSet(labels.Set{"app": "kube-apiserver"})
 			c := &CertificateRevocationController{
 				listPods: func(namespace string, selector labels.Selector) ([]*corev1.Pod, error) {
+					if namespace != "test-ns" {
+						t.Fatalf("unexpected namespace: %q", namespace)
+					}
+					if selector.String() != expectedSelector.String() {
+						t.Fatalf("unexpected selector: %q", selector.String())
+					}
 					return testCase.pods, nil
 				},
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go`
around lines 1376 - 1378, The test's listPods mock currently ignores its
namespace and selector parameters; update the mock used in the test case for
listPods to validate that the incoming namespace and selector match the expected
values for that test (e.g., compare against testCase.expectedNamespace and
testCase.expectedSelector or derive from testCase.namespace/testCase.selector)
and return an error if they differ, otherwise return testCase.pods; this will
ensure regressions in namespace/selector handling are caught by the test.

1246-1373: Add explicit cases for missing PodIP and error propagation branches.

The table currently misses the PodIP == "" requeue path and error-return branches (listPods error and testFunc error). Covering these will harden regression detection in this critical flow.

Suggested additions (table-driven cases)
 	for _, testCase := range []struct {
 		name     string
 		pods     []*corev1.Pod
 		testFunc func(ctx context.Context, client kubernetes.Interface) (bool, error)
+		listPodsErr error
 
 		expectedResult bool
 		expectedErr    bool
 		expectedCalls  int
 	}{
+		{
+			name: "When a ready pod has empty PodIP it should requeue",
+			pods: []*corev1.Pod{{
+				ObjectMeta: metav1.ObjectMeta{Name: "kas-1", Namespace: "test-ns"},
+				Spec:       kasPodSpec(),
+				Status: corev1.PodStatus{
+					PodIP: "",
+					Conditions: []corev1.PodCondition{{
+						Type:   corev1.PodReady,
+						Status: corev1.ConditionTrue,
+					}},
+				},
+			}},
+			expectedResult: false,
+			expectedCalls:  0,
+		},
+		{
+			name:          "When listing pods fails it should return an error",
+			listPodsErr:   assert.AnError,
+			expectedErr:   true,
+			expectedCalls: 0,
+		},
+		{
+			name: "When testFunc returns an error it should return an error",
+			pods: []*corev1.Pod{{
+				ObjectMeta: metav1.ObjectMeta{Name: "kas-1", Namespace: "test-ns"},
+				Spec:       kasPodSpec(),
+				Status: corev1.PodStatus{
+					PodIP: "10.0.0.1",
+					Conditions: []corev1.PodCondition{{Type: corev1.PodReady, Status: corev1.ConditionTrue}},
+				},
+			}},
+			testFunc: func(_ context.Context, _ kubernetes.Interface) (bool, error) {
+				return false, assert.AnError
+			},
+			expectedErr:   true,
+			expectedCalls: 1,
+		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go`
around lines 1246 - 1373, Add three table-driven test cases to exercise the
missing branches: (1) a "pod with empty PodIP should requeue" case where pods
contains a Pod whose Status.PodIP == "" and expectedResult is false; (2) a
"listPods returns error" case that simulates listPods failing (set test setup to
return an error when listing pods) and set expectedErr true; and (3) a "testFunc
returns error" case where testFunc returns (false, error) and expectedErr true.
Place these entries alongside the existing cases in the same test table (the
loop over testCase) so the existing test runner uses them; reference the
existing fields testFunc and expectedErr to validate behavior and the listPods
path to simulate the list failure. Ensure the case names are descriptive and
expectedResult/expectedCalls are set appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go`:
- Around line 1376-1378: The test's listPods mock currently ignores its
namespace and selector parameters; update the mock used in the test case for
listPods to validate that the incoming namespace and selector match the expected
values for that test (e.g., compare against testCase.expectedNamespace and
testCase.expectedSelector or derive from testCase.namespace/testCase.selector)
and return an error if they differ, otherwise return testCase.pods; this will
ensure regressions in namespace/selector handling are caught by the test.
- Around line 1246-1373: Add three table-driven test cases to exercise the
missing branches: (1) a "pod with empty PodIP should requeue" case where pods
contains a Pod whose Status.PodIP == "" and expectedResult is false; (2) a
"listPods returns error" case that simulates listPods failing (set test setup to
return an error when listing pods) and set expectedErr true; and (3) a "testFunc
returns error" case where testFunc returns (false, error) and expectedErr true.
Place these entries alongside the existing cases in the same test table (the
loop over testCase) so the existing test runner uses them; reference the
existing fields testFunc and expectedErr to validate behavior and the listPods
path to simulate the list failure. Ensure the case names are descriptive and
expectedResult/expectedCalls are set appropriately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 40f42242-fce3-4416-beb2-928fa3c63249

📥 Commits

Reviewing files that changed from the base of the PR and between c345009 and 0e87b3d.

📒 Files selected for processing (1)
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go (1)

601-622: ⚠️ Potential issue | 🟠 Major

Add explicit timeout to prevent hanging on unresponsive pods.

The per-pod client probes inherit Timeout=0 from adminCfg (meaning no timeout). A blackholed or half-open PodIP can hang the reconcile worker indefinitely since rest.Config.Timeout=0 means "no timeout" in client-go.

🛡️ Proposed fix
+	const perPodTimeout = 10 * time.Second
+
 	for _, pod := range readyPods {
 		port := containerPort(pod, "client", config.KASPodDefaultPort)
 		podCfg := rest.AnonymousClientConfig(adminCfg)
+		podCfg.Timeout = perPodTimeout
 		podCfg.TLSClientConfig.CertData = certPEM
 		podCfg.TLSClientConfig.KeyData = keyPEM
 		podCfg.Host = fmt.Sprintf("https://%s", net.JoinHostPort(pod.Status.PodIP, strconv.Itoa(int(port))))
 		// We're connecting to the PodIP, but the serving cert is still issued for the KAS service
 		// name. Keep CA verification enabled and override ServerName for SNI + hostname validation.
 		podCfg.TLSClientConfig.ServerName = hcpmanifests.KubeAPIServerServiceName

 		podClient, err := kubernetes.NewForConfig(podCfg)
 		if err != nil {
 			return false, fmt.Errorf("couldn't create client for KAS pod %s/%s: %w", pod.Namespace, pod.Name, err)
 		}

-		passed, err := testFunc(ctx, podClient)
+		podCtx, cancel := context.WithTimeout(ctx, perPodTimeout)
+		passed, err := testFunc(podCtx, podClient)
+		cancel() // explicit cleanup instead of defer inside loop
 		if err != nil {
 			return false, err
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`
around lines 601 - 622, The pod-specific REST client inherits a zero (no)
timeout from rest.AnonymousClientConfig(adminCfg), which can hang on blackholed
PodIPs; set an explicit timeout on the returned rest.Config (podCfg.Timeout)
before calling kubernetes.NewForConfig — e.g. use a sane default like 15–30s or
copy adminCfg.Timeout when non-zero — and ensure you import time if using a
time.Duration literal; update the code around
rest.AnonymousClientConfig(adminCfg) / podCfg and before kubernetes.NewForConfig
to assign podCfg.Timeout.
🧹 Nitpick comments (1)
control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go (1)

584-599: Consider adding debug logging for requeue reasons.

When requeuing because a pod isn't ready or no pods are available, it may be helpful for debugging to log which pod or condition triggered the requeue. This is optional since the behavior is correct.

💡 Optional: Add debug logging
 	var readyPods []*corev1.Pod
 	for _, pod := range pods {
 		if pod.DeletionTimestamp != nil {
 			continue
 		}
 		if !isPodReady(pod) || pod.Status.PodIP == "" {
 			// a non-terminating pod that's not ready: we can't check it yet, requeue
+			klog.V(4).Infof("KAS pod %s/%s not ready for verification (ready=%v, podIP=%q), requeueing", 
+				pod.Namespace, pod.Name, isPodReady(pod), pod.Status.PodIP)
 			return false, nil
 		}
 		readyPods = append(readyPods, pod)
 	}

 	if len(readyPods) == 0 {
 		// no pods to check yet, requeue
+		klog.V(4).Infof("No ready KAS pods found in namespace %s, requeueing", namespace)
 		return false, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`
around lines 584 - 599, Add debug logs before the two early returns to record
why we're requeuing: inside the loop, when skipping a pod because
!isPodReady(pod) or pod.Status.PodIP == "" log the pod's name/namespace and the
specific condition that triggered the requeue (use pod.Name, pod.Namespace,
isPodReady result and pod.Status.PodIP); and after the loop, when len(readyPods)
== 0 log that no non-terminating ready pods were found (include the original
pods list size or selector info if available). Use the controller's existing
logger (e.g., r.Log or reqLogger) to emit these debug messages right before the
respective return false, nil statements.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`:
- Around line 601-622: The pod-specific REST client inherits a zero (no) timeout
from rest.AnonymousClientConfig(adminCfg), which can hang on blackholed PodIPs;
set an explicit timeout on the returned rest.Config (podCfg.Timeout) before
calling kubernetes.NewForConfig — e.g. use a sane default like 15–30s or copy
adminCfg.Timeout when non-zero — and ensure you import time if using a
time.Duration literal; update the code around
rest.AnonymousClientConfig(adminCfg) / podCfg and before kubernetes.NewForConfig
to assign podCfg.Timeout.

---

Nitpick comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`:
- Around line 584-599: Add debug logs before the two early returns to record why
we're requeuing: inside the loop, when skipping a pod because !isPodReady(pod)
or pod.Status.PodIP == "" log the pod's name/namespace and the specific
condition that triggered the requeue (use pod.Name, pod.Namespace, isPodReady
result and pod.Status.PodIP); and after the loop, when len(readyPods) == 0 log
that no non-terminating ready pods were found (include the original pods list
size or selector info if available). Use the controller's existing logger (e.g.,
r.Log or reqLogger) to emit these debug messages right before the respective
return false, nil statements.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: a2b128b1-8046-4168-956c-a837c093fb68

📥 Commits

Reviewing files that changed from the base of the PR and between 0e87b3d and 3ac77d1.

⛔ Files ignored due to path filters (5)
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/AROSwift/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/GCP/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/IBMCloud/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
  • control-plane-operator/controllers/hostedcontrolplane/testdata/control-plane-pki-operator/zz_fixture_TestControlPlaneComponents_control_plane_pki_operator_role.yaml is excluded by !**/testdata/**
📒 Files selected for processing (3)
  • control-plane-operator/controllers/hostedcontrolplane/v2/assets/control-plane-pki-operator/role.yaml
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go
✅ Files skipped from review due to trivial changes (1)
  • control-plane-operator/controllers/hostedcontrolplane/v2/assets/control-plane-pki-operator/role.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go

@sdminonne sdminonne changed the title fix(OCPBUGS-62177): verify cert revocation against all KAS pods OCPBUGS-62177: verify cert revocation against all KAS pods Apr 22, 2026
@openshift-ci-robot openshift-ci-robot added jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. labels Apr 22, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@sdminonne: This pull request references Jira Issue OCPBUGS-62177, which is invalid:

  • expected the bug to target either version "5.0." or "openshift-5.0.", but it targets "4.22" instead

Comment /jira refresh to re-evaluate validity if changes to the Jira bug are made, or edit the title of this pull request to link to a different bug.

The bug has been updated to refer to the pull request using the external bug tracker.

Details

In response to this:

Summary

  • Add per-pod verification to CertificateRevocationController so that certificate trust/revocation checks connect to each KAS pod individually rather than through the service load balancer, preventing premature advancement of the revocation flow in HA setups
  • Grant list/watch on pods to the control-plane-pki-operator RBAC role (and update all 5 test fixtures)
  • Add unit tests for isPodReady, containerPort, and verifyCertificateAgainstAllKASPods

Context

The controller's ensureNewSignerCertificatePropagated and ensureOldSignerCertificateRevoked previously verified certificate trust/revocation via the KAS service endpoint. In HA setups with multiple KAS pods, the service load-balances to a single pod, so the check could hit a pod that had loaded the updated trust bundle while others hadn't, causing premature state transitions.

This was previously fixed in PRs #7405 + #7744 but reverted in #7784 because the e2e test added alongside it (TestCreateClusterRequestServingIsolation) had a ~50% flake rate. The controller fix itself was sound — the flakiness was in the e2e test wiring. This PR re-applies only the controller logic with unit tests, without the flaky e2e test.

Test plan

  • go test -v -race ./control-plane-pki-operator/certificaterevocationcontroller/... — all pass
  • go test -v -run TestControlPlaneComponents ./control-plane-operator/controllers/hostedcontrolplane/ — passes (validates RBAC fixture changes)
  • Existing integration test TestControlPlanePKIOperatorBreakGlassCredentials covers the full CRR flow
  • Existing e2e test TestCreateCluster covers single-replica CRR flow

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

  • Improved API server certificate propagation and revocation by validating trust/rejection on each active API-server pod.

  • Broadened operator permissions to allow listing and watching pod resources for reliable monitoring.

  • Tests

  • Added unit tests for pod readiness, container-port resolution, and per-pod certificate verification behaviors.

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.

@sdminonne
sdminonne marked this pull request as ready for review April 23, 2026 05:53
@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 Apr 23, 2026
@openshift-ci
openshift-ci Bot requested review from cblecker and csrwng April 23, 2026 05:53

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go (1)

99-100: Wire pod updates into the queue, not just the cache.

The new gate now depends on pod readiness and PodIP, but this informer is only being initialized for lister access. That means CRRs still converge on those transitions only via SyntheticRequeueError. Hooking pod events into enqueueAll(...) would make rollouts react faster and cut needless requeues while KAS pods are coming up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`
around lines 99 - 100, The Pod informer is only created for lister access but
not wired to enqueue changes, so Pod readiness/PodIP transitions don't trigger
immediate reconciles; update the code around
kubeInformersForNamespaces.InformersFor(hostedControlPlane.Namespace).Core().V1().Pods().Informer()
to add event handlers that call enqueueAll(...) on Pod Add/Update/Delete (use
cache.ResourceEventHandlerFuncs with AddFunc, UpdateFunc that compares old/new
and always call enqueueAll for relevant changes, and DeleteFunc) so Pod events
drive the controller rather than relying on SyntheticRequeueError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go`:
- Around line 99-100: The Pod informer is only created for lister access but not
wired to enqueue changes, so Pod readiness/PodIP transitions don't trigger
immediate reconciles; update the code around
kubeInformersForNamespaces.InformersFor(hostedControlPlane.Namespace).Core().V1().Pods().Informer()
to add event handlers that call enqueueAll(...) on Pod Add/Update/Delete (use
cache.ResourceEventHandlerFuncs with AddFunc, UpdateFunc that compares old/new
and always call enqueueAll for relevant changes, and DeleteFunc) so Pod events
drive the controller rather than relying on SyntheticRequeueError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b0d6c3c1-3ba0-403f-bbf5-2f676ef56184

📥 Commits

Reviewing files that changed from the base of the PR and between 3ac77d1 and 135f5b1.

📒 Files selected for processing (2)
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller.go
  • control-plane-pki-operator/certificaterevocationcontroller/certificaterevocationcontroller_test.go

@sdminonne
sdminonne force-pushed the OCPBUGS-62177 branch 2 times, most recently from 146e31a to 5b7f272 Compare April 23, 2026 06:45
@sdminonne

Copy link
Copy Markdown
Contributor Author

/jira refresh

@openshift-ci-robot openshift-ci-robot added the jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. label Apr 23, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@sdminonne: This pull request references Jira Issue OCPBUGS-62177, which is valid. The bug has been moved to the POST state.

3 validation(s) were run on this bug
  • bug is open, matching expected state (open)
  • bug target version (5.0.0) matches configured target version for branch (5.0.0)
  • bug is in the state ASSIGNED, which is one of the valid states (NEW, ASSIGNED, POST)

No GitHub users were found matching the public email listed for the QA contact in Jira (jiezhao@redhat.com), skipping review request.

Details

In response to this:

/jira refresh

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-robot openshift-ci-robot removed the jira/invalid-bug Indicates that a referenced Jira bug is invalid for the branch this PR is targeting. label Apr 23, 2026
@sdminonne
sdminonne marked this pull request as draft April 23, 2026 06:52
@sdminonne

Copy link
Copy Markdown
Contributor Author

/test e2e-aks-4-22

@hypershift-jira-solve-ci

Copy link
Copy Markdown
Contributor

AI Test Failure Analysis

Job: pull-ci-openshift-hypershift-main-e2e-aws | Build: 2053551352042229760 | Cost: $4.418569600000001 | Failed step: hypershift-aws-run-e2e-nested

View full analysis report


Generated by hypershift-analyze-e2e-failure post-step using Claude claude-opus-4-6

@hypershift-jira-solve-ci

Copy link
Copy Markdown
Contributor

Test Failure Analysis: pull-ci-openshift-hypershift-main-e2e-aws / Build 2053551352042229760

Job: pull-ci-openshift-hypershift-main-e2e-aws
Build ID: 2053551352042229760
Target: e2e-aws
PR: #8263OCPBUGS-62177: verify cert revocation against all KAS pods
Result: 5 failures / 566 tests (26 skipped)


Failure Verdict: Pre-existing test flake — unrelated to PR changes

All 5 failures originate from a single root cause in the EnsureGlobalPullSecret test. The PR's own tests (TestCreateClusterHABreakGlassCredentials, break-glass-credentials subtests) all passed.


Root Cause

The kubelet-config-verifier DaemonSet created by VerifyKubeletConfigWithDaemonSet() in test/e2e/util/globalps.go scheduled 3 pods (one per node) but only 2 ever became ready. The test polled for ~20 minutes (360+ iterations) before hitting context deadline exceeded.

Why the cascade: The function uses a bare g.Expect(waitForDaemonSetReady(...)).To(Succeed()) assertion (line 220) which immediately halts execution on failure — the cleanup code on lines 223-239 (delete DaemonSet + pull-secret) is never reached. The next subtest (Check_if_the_config.json_is_correct_in_all_of_the_nodes) then calls CreateKubeletConfigVerifierDaemonSet() which attempts to create the same DaemonSet and gets a 409 Conflict: daemonsets.apps "kubelet-config-verifier" already exists.

Why 2/3 pods: The NodePool has replicas: 1 but the cluster has 3 actual nodes. The DaemonSet schedules to all 3 nodes, but one pod perpetually fails to reach Ready. The 20-minute timeout is insufficient for recovery.

Design flaw: VerifyKubeletConfigWithDaemonSet() does not use defer for cleanup. If the readiness wait fails, stale resources remain and poison subsequent subtests.

5 failing test cases (all from one root cause):

# Test Duration Direct Error
1 EnsureGlobalPullSecret/When_management-cluster_hostedCluster.Spec.PullSecret_is_updated_in-place… 1215s context deadline exceeded waiting for DaemonSet
2 EnsureGlobalPullSecret/Check_if_the_config.json_is_correct_in_all_of_the_nodes 0.02s 409: daemonsets.apps "kubelet-config-verifier" already exists
3 EnsureGlobalPullSecret Parent cascade
4 Main Parent cascade
5 TestCreateCluster Parent cascade
Recommendations
  1. Move cleanup to defer in VerifyKubeletConfigWithDaemonSet() (globalps.go:210-240). The DaemonSet deletion and pull-secret cleanup must execute regardless of whether waitForDaemonSetReady succeeds:

    ds, err := CreateKubeletConfigVerifierDaemonSet(...)
    g.Expect(err).ToNot(HaveOccurred())
    defer func() {
        _ = client.AppsV1().DaemonSets(ds.Namespace).Delete(ctx, ds.Name, metav1.DeleteOptions{})
        _ = client.CoreV1().Secrets(ds.Namespace).Delete(ctx, "pull-secret", metav1.DeleteOptions{})
    }()
  2. Handle AlreadyExists for the DaemonSet in CreateKubeletConfigVerifierDaemonSet() (line 200). The function already handles AlreadyExists for the pull-secret (lines 47-50) but not for the DaemonSet itself. Add the same pattern: delete-and-recreate or return the existing object.

  3. Investigate the 2/3 DaemonSet readiness. The replicas: 1 NodePool with 3 actual nodes suggests either autoscaling or a stale NodePool spec. The DaemonSet should tolerate node count mismatches or the test should pin expected pod count to the actual schedulable node count.

  4. This PR is safe to merge from a test perspective — the failures are a known flake in EnsureGlobalPullSecret unrelated to cert revocation changes.

Evidence

build-log.txt — DaemonSet stuck at 2/3 ready (~360 poll iterations):

waiting for DaemonSet kubelet-config-verifier to be ready, 2/3 are ready
waiting for DaemonSet kubelet-config-verifier to be ready, 2/3 are ready
...
(repeats for ~20 minutes)

build-log.txt — Timeout error (globalps.go:220):

failed to wait for DaemonSet kubelet-config-verifier to be ready: context deadline exceeded

build-log.txt — Cascade error (globalps.go:211):

daemonsets.apps "kubelet-config-verifier" already exists

junit.xml — Failure 1 (timeout):

<testcase name="When_management-cluster_hostedCluster.Spec.PullSecret_is_updated_in-place..."
          classname="TestCreateCluster/Main/EnsureGlobalPullSecret" time="1215.12">
  <failure>failed to wait for DaemonSet kubelet-config-verifier to be ready: context deadline exceeded</failure>
</testcase>

junit.xml — Failure 2 (stale resource):

<testcase name="Check_if_the_config.json_is_correct_in_all_of_the_nodes"
          classname="TestCreateCluster/Main/EnsureGlobalPullSecret" time="0.02">
  <failure>daemonsets.apps &quot;kubelet-config-verifier&quot; already exists</failure>
</testcase>

globalps.go:220 — Non-deferred assertion (root of cascade):

g.Expect(waitForDaemonSetReady(ctx, client, ds)).To(Succeed())
// Lines 223-239: cleanup code UNREACHABLE on failure

PR tests — All passed:

  • TestCreateClusterHABreakGlassCredentials — PASSED (1911.95s)
  • All break-glass-credentials subtests — PASSED

@sdminonne

Copy link
Copy Markdown
Contributor Author

/retest-required

@sdminonne

Copy link
Copy Markdown
Contributor Author

--- PASS: TestCreateClusterHABreakGlassCredentials/ValidateHostedCluster (670.23s)

@sdminonne

Copy link
Copy Markdown
Contributor Author

/verified by e2e

@openshift-ci-robot openshift-ci-robot added the verified Signifies that the PR passed pre-merge verification criteria label May 11, 2026
@openshift-ci-robot

Copy link
Copy Markdown

@sdminonne: This PR has been marked as verified by e2e.

Details

In response to this:

/verified by e2e

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.

}
hasWorkerNodes = len(nodeList.Items) > 0
} else {
// Private clusters are not reachable from the test runner;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@csrwng FYI

@csrwng

csrwng commented May 11, 2026

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label May 11, 2026
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Tests from second stage were triggered manually. Pipeline can be controlled only manually, until HEAD changes. Use command to trigger second stage.

@csrwng

csrwng commented May 11, 2026

Copy link
Copy Markdown
Contributor

/test-required

@openshift-ci

openshift-ci Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

@sdminonne: all tests passed!

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.

@openshift-merge-bot
openshift-merge-bot Bot merged commit 1cec72f into openshift:main May 11, 2026
43 checks passed
@openshift-ci-robot

Copy link
Copy Markdown

@sdminonne: Jira Issue Verification Checks: Jira Issue OCPBUGS-62177
✔️ This pull request was pre-merge verified.
✔️ All associated pull requests have merged.
✔️ All associated, merged pull requests were pre-merge verified.

Jira Issue OCPBUGS-62177 has been moved to the MODIFIED state and will move to the VERIFIED state when the change is available in an accepted nightly payload. 🕓

Details

In response to this:

What this PR does / why we need it:

Updates the CertificateRevocationController to verify certificate trust and revocation against every individual KAS pod rather than through the service load balancer. In HA deployments with 3 KAS replicas, the service endpoint load-balances to a single pod, so the check could hit a pod that had loaded the updated trust bundle while others hadn't, causing premature state transitions in the revocation flow.

Changes

control-plane-pki-operator (CertificateRevocationController):

  • Add verifyCertificateAgainstAllKASPods which enumerates ready KAS pods via label selector and connects to each pod IP directly (with SNI set to the KAS service name for TLS validation)
  • Cross-check ready pod count against the KAS Deployment's expected replica count to prevent verifying only a subset during informer cache lag or pod restarts
  • Add dual-cert cross-checks: in ensureNewSignerCertificatePropagated, verify the old signer is also still trusted (detects mid-reload); in ensureOldSignerCertificateRevoked, verify the new signer cert is still trusted before declaring the old one revoked, preventing false positives when a pod restart temporarily rejects all certs
  • Wire a KAS pod informer so pod readiness changes trigger immediate reconciliation
  • Extract verifyCertificateTrusted and verifyCertificateRevoked helpers for per-pod SelfSubjectReview checks

control-plane-operator (RBAC):

  • Grant list/watch on pods and get on deployments to the control-plane-pki-operator role so it can enumerate KAS pods and verify replica counts (update role asset + 5 test fixtures)

support/podspec:

  • Add IsPodReady and ContainerPort helpers for pod readiness checks and named port lookup

e2e / integration:

  • Add TestCreateClusterHABreakGlassCredentials exercising the break-glass credential flow on a HighlyAvailable control plane (3 KAS replicas)
  • Add per-request timeout to revocation SSR polling in the integration test to prevent indefinite hangs when KAS stalls the TLS handshake during trust bundle reload
  • Fix hasWorkerNodes detection for private clusters by checking NodePool replica counts instead of assuming workers exist

Context

This was previously fixed in PRs #7405 + #7744 but reverted in #7784 because the e2e test added alongside it had a ~50% flake rate. The controller fix itself was sound — the flakiness was in the e2e test wiring. This PR re-applies the controller logic with comprehensive unit tests and a different, more targeted HA e2e test.

Which issue(s) this PR fixes:

Fixes https://issues.redhat.com/browse/OCPBUGS-62177

Special notes for your reviewer:

The dual-cert cross-checks are the key addition beyond the original fix. In ensureOldSignerCertificateRevoked, verifying the new cert is trusted before declaring the old one revoked prevents false positives when a KAS pod restart during revocation causes the old cert to become unreachable (connection refused), which looks like revocation but is actually the pod being down. Similarly, in ensureNewSignerCertificatePropagated, verifying the old cert is also still trusted prevents declaring propagation complete when a pod is mid-reload and happens to accept the new cert but hasn't finished loading the full trust bundle.

Checklist:

  • Subject and description added to both, commit and PR.
  • Relevant issues have been referenced.
  • This change includes docs.
  • This change includes unit tests.
  • This change includes e2e.

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.

@sdminonne

Copy link
Copy Markdown
Contributor Author

@openshift-merge-robot

Copy link
Copy Markdown
Contributor

Fix included in release 5.0.0-0.nightly-2026-05-12-025124

@sdminonne

Copy link
Copy Markdown
Contributor Author

/cherry-pick release-4.20

@openshift-cherrypick-robot

Copy link
Copy Markdown

@sdminonne: #8263 failed to apply on top of branch "release-4.20":

Applying: feat(hypershift-operator): add pod readiness and container port helpers
Using index info to reconstruct a base tree...
A	support/podspec/containers.go
A	support/podspec/containers_test.go
Falling back to patching base and 3-way merge...
Auto-merging support/util/containers.go
CONFLICT (content): Merge conflict in support/util/containers.go
CONFLICT (modify/delete): support/podspec/containers_test.go deleted in HEAD and modified in feat(hypershift-operator): add pod readiness and container port helpers. Version feat(hypershift-operator): add pod readiness and container port helpers of support/podspec/containers_test.go left in tree.
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0001 feat(hypershift-operator): add pod readiness and container port helpers

Details

In response to this:

/cherry-pick release-4.20

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.

@sdminonne

Copy link
Copy Markdown
Contributor Author

/cherry-pick release-4.22

@openshift-cherrypick-robot

Copy link
Copy Markdown

@sdminonne: #8263 failed to apply on top of branch "release-4.22":

Applying: feat(hypershift-operator): add pod readiness and container port helpers
Using index info to reconstruct a base tree...
A	support/podspec/containers.go
A	support/podspec/containers_test.go
Falling back to patching base and 3-way merge...
Auto-merging support/util/containers_test.go
CONFLICT (content): Merge conflict in support/util/containers_test.go
Auto-merging support/util/containers.go
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0001 feat(hypershift-operator): add pod readiness and container port helpers

Details

In response to this:

/cherry-pick release-4.22

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.

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/control-plane-pki-operator Indicates the PR includes changes for the control plane PKI operator - in an OCP release area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release area/testing Indicates the PR includes changes for e2e testing jira/valid-bug Indicates that a referenced Jira bug is valid for the branch this PR is targeting. jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. lgtm Indicates that a PR is ready to be merged. verified Signifies that the PR passed pre-merge verification criteria

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants