Skip to content

OCPBUGS-77827 fix(api): add missing has() check for servingCerts in validation#7901

Draft
eponafyrefly wants to merge 1 commit intoopenshift:mainfrom
eponafyrefly:fix-servingcert-validation-regression
Draft

OCPBUGS-77827 fix(api): add missing has() check for servingCerts in validation#7901
eponafyrefly wants to merge 1 commit intoopenshift:mainfrom
eponafyrefly:fix-servingcert-validation-regression

Conversation

@eponafyrefly
Copy link
Copy Markdown

@eponafyrefly eponafyrefly commented Mar 10, 2026

What this PR does / why we need it:

PR #6091 introduced a validation rule that checks if the APIServer
loadBalancer hostname conflicts with namedCertificates. However, the
validation directly accesses configuration.apiServer.servingCerts.namedCertificates
without checking if the optional servingCerts field exists.

This causes CEL validation failures when patching HostedCluster resources
that don't have the servingCerts field populated.

Add has(self.configuration.apiServer.servingCerts) check before
accessing namedCertificates field to prevent validation errors on resources without this optional field.

Which issue(s) this PR fixes:

OCPBUGS-77827 - Validation of hostedcluster CRD servingCerts causes a CEL rule failure when patched

Special notes for your reviewer:

Checklist:

  • Subject and description added to both, commit and PR.

  • Relevant issues have been referenced.

  • This change includes docs.

  • This change includes unit tests.

    • Claude Sonnet 4.5 was used to assist in searching codebase for the validation.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced validation logic for API server configuration to properly handle edge cases when serving certificates are not configured, preventing validation errors in certain scenarios.
  • Refactor

    • Improved robustness of configuration validation rules across multiple manifest definitions by adding additional safety checks during field validation.

  PR openshift#6091 introduced a CEL validation rule that checks if the APIServer
  loadBalancer hostname conflicts with namedCertificates. However, the
  validation directly accesses configuration.apiServer.servingCerts.namedCertificates
  without checking if the optional servingCerts field exists.

  This causes validation failures when patching HostedCluster resources
  that don't have the servingCerts field populated.

  Add has(self.configuration.apiServer.servingCerts) check before
  accessing namedCertificates to fix the regression.
@openshift-ci-robot
Copy link
Copy Markdown

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 Mar 10, 2026
@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Mar 10, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: eponafyrefly
Once this PR has been reviewed and has the lgtm label, please assign devguyio 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

@openshift-ci openshift-ci Bot added area/api Indicates the PR includes changes for the API area/cli Indicates the PR includes changes for CLI and removed do-not-merge/needs-area labels Mar 10, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 10, 2026

Walkthrough

The pull request adds defensive null-check guards to the APIServer loadBalancer hostname validation rule across CRD manifests. The change ensures servingCerts exists before accessing its namedCertificates field, preventing potential nil dereferences when serving certificates are not configured.

Changes

Cohort / File(s) Summary
Source Validation Rule
api/hypershift/v1beta1/hostedcluster_types.go
Added explicit has(self.configuration.apiServer.servingCerts) guard before accessing namedCertificates in APIServer loadBalancer hostname validation.
Generated Feature-Gated CRD Manifests
api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/*
Applied the same null-check guard to 14 feature-gated variant manifests (AAA_ungated, AutoNodeKarpenter, ClusterUpdateAcceptRisks, ClusterVersionOperatorConfiguration, ExternalOIDC, ExternalOIDCWithUIDAndExtraClaimMappings, ExternalOIDCWithUpstreamParity, GCPPlatform, HyperShiftOnlyDynamicResourceAllocation, ImageStreamImportMode, KMSEncryptionProvider, NetworkDiagnosticsConfig, OpenStack).
Generated Default CRD Manifests
cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-*
Applied the same defensive guard to 3 default CRD manifest variants (CustomNoUpgrade, Default, TechPreviewNoUpgrade).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately describes the main change: adding missing has() checks for servingCerts in validation rules across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Stable And Deterministic Test Names ✅ Passed No test files with test titles found; only Go type definitions and Kubernetes CRD manifest modifications.
Test Structure And Quality ✅ Passed This custom check is not applicable to this PR because it does not contain any Ginkgo test code.

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

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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Mar 10, 2026

Hi @eponafyrefly. Thanks for your PR.

I'm waiting for a openshift member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

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.

@openshift-ci openshift-ci Bot added the needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. label Mar 10, 2026
@eponafyrefly
Copy link
Copy Markdown
Author

/test all

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Mar 10, 2026

@eponafyrefly: Cannot trigger testing until a trusted user reviews the PR and leaves an /ok-to-test message.

Details

In response to this:

/test all

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.

Copy link
Copy Markdown
Contributor

@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: 7

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

Inline comments:
In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml`:
- Around line 6004-6006: The CEL expression dereferences optional fields without
guards—specifically self.configuration.apiServer.servingCerts.namedCertificates
and cert.names—so update the predicate to wrap those accesses with has() checks
(e.g., has(self.configuration.apiServer.servingCerts.namedCertificates) and
has(cert.names)) before calling .exists; adjust the inner exists() lambdas (the
one iterating namedCertificates and the cert.names.exists(...) call) to only run
when those has() checks pass so you don't access missing fields in the
namedCertificates and cert.names references.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml`:
- Around line 6355-6356: The CEL expression calls .exists() on
self.configuration.apiServer.servingCerts.namedCertificates without first
checking the field exists; update the boolean guard by adding
has(self.configuration.apiServer.servingCerts.namedCertificates) before calling
.exists() so the expression becomes ... && has(self.configuration.apiServer) &&
has(self.configuration.apiServer.servingCerts) &&
has(self.configuration.apiServer.servingCerts.namedCertificates) &&
self.configuration.apiServer.servingCerts.namedCertificates.exists(...),
ensuring namedCertificates is present before dereferencing in the validation
rule.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml`:
- Around line 6365-6369: The CEL validation rule currently dereferences optional
fields without guarding them; update the rule string so each optional access is
protected: add a has(s.servicePublishingStrategy.loadBalancer.hostname) (or
check hostname != "" only after confirming has(loadBalancer) and has(hostname)),
add has(self.configuration.apiServer.servingCerts.namedCertificates) before
calling namedCertificates.exists(...), and guard cert.names with has(cert.names)
(or ensure cert.names exists/non-empty) before calling cert.names.exists; keep
the existing checks (e.g., has(self.configuration) and
has(self.configuration.apiServer) and
has(self.configuration.apiServer.servingCerts)) and apply these additional
has(...) checks around s.servicePublishingStrategy.loadBalancer.hostname,
self.configuration.apiServer.servingCerts.namedCertificates, and cert.names in
the CEL rule.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml`:
- Around line 6009-6010: The CEL expression dereferences an optional field:
before calling
self.configuration.apiServer.servingCerts.namedCertificates.exists(cert, ...)
you must guard namedCertificates with
has(self.configuration.apiServer.servingCerts.namedCertificates); update the
condition that currently checks has(self.configuration.apiServer) &&
has(self.configuration.apiServer.servingCerts) to also include
has(self.configuration.apiServer.servingCerts.namedCertificates) so the call to
.exists() on namedCertificates is only evaluated when that field is present.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml`:
- Around line 6538-6540: The CEL rule dereferences two optional fields without
presence checks: self.configuration.apiServer.servingCerts.namedCertificates and
cert.names; update the expression used in the rule to guard those accesses with
has() checks (e.g., add
has(self.configuration.apiServer.servingCerts.namedCertificates) before
iterating namedCertificates and add has(cert.names) before calling
cert.names.exists) so the overall condition only evaluates those sub-expressions
when those optional fields are present.

In
`@cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml`:
- Around line 6698-6700: The CEL expression dereferences an optional field:
guard self.configuration.apiServer.servingCerts.namedCertificates before calling
.exists(); update the predicate that currently uses
self.configuration.apiServer.servingCerts.namedCertificates.exists(cert, ...) to
first check has(self.configuration.apiServer.servingCerts.namedCertificates) &&
then call .exists(...). Apply the same change to the identical expressions in
hostedclusters-Hypershift-Default.crd.yaml,
hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml, and
hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml so all occurrences check
namedCertificates presence before .exists().

In
`@cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml`:
- Around line 7675-7677: The CEL validation currently checks
self.configuration.apiServer.servingCerts but calls .exists() on two optional
lists without guarding them: servingCerts.namedCertificates.exists(...) and
cert.names.exists(...); update the rule to first check
has(self.configuration.apiServer.servingCerts.namedCertificates) before calling
namedCertificates.exists(...) and has(cert.names) before calling
cert.names.exists(...) so both optional lists are guarded and CEL evaluation
won't fail when those lists are absent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b02f72d4-f9bd-4b1c-8424-0a6f937d158f

📥 Commits

Reviewing files that changed from the base of the PR and between 7604733 and 6872b05.

⛔ Files ignored due to path filters (1)
  • vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go is excluded by !**/vendor/**, !vendor/**
📒 Files selected for processing (17)
  • api/hypershift/v1beta1/hostedcluster_types.go
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryptionProvider.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml
  • cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml
  • cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml
  • cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml

Comment on lines +6004 to 6006
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant section
head -c 500000 api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml | tail -c 50000 | tail -n 100

Repository: openshift/hypershift

Length of output: 5603


🏁 Script executed:

# Get the exact lines mentioned in the review
sed -n '5990,6020p' api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml

Repository: openshift/hypershift

Length of output: 2471


🏁 Script executed:

# Search for the schema definition of servingCerts to see if namedCertificates is required
sed -n '1,6000p' api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml | grep -A 50 "servingCerts" | head -100

Repository: openshift/hypershift

Length of output: 3530


🌐 Web query:

Kubernetes CEL optional fields has() guard namedCertificates exists() semantics

💡 Result:

has() (presence guard) in Kubernetes CEL

  • has(e.f) is a CEL macro used to test presence / availability of a field before you rely on it. Kubernetes explicitly calls out has() as the way to check whether a field of a dynamically typed value is accessible (to avoid errors when the field might not exist in the schema for the object you’re evaluating). (kubernetes.io)
  • Precise CEL-spec semantics depend on the underlying type of e (github.com):
    • Map: has(e.f) is true iff key "f" exists (even if the value is null; null ≠ absence in CEL). (github.com)
    • Message / object
      • If f is repeated (list) or map field: has(e.f) is true iff it is non-empty. (github.com)
      • For proto3 scalar fields, has(e.f) can effectively mean “value is not the default” (because proto3 historically can’t always distinguish “unset” from “set-to-default”). (github.com)

Implication for namedCertificates (a list field):
has(self.namedCertificates) is true only when the list is non-empty; it’s false for both “missing” and “present but empty”. (github.com)


exists() semantics (including error/short-circuit behavior)

  • list.exists(x, p) returns true if any element satisfies predicate p; for an empty list it returns false. (github.com)
  • exists() “behaves like logical OR (||)” regarding short-circuiting and error absorption: if some element makes the result true, errors from other elements/predicates can be ignored (consistent with CEL’s OR behavior). (github.com)

So for namedCertificates:

  • self.namedCertificates.exists(c, /*match*/) is false when the list is empty (and typically also when it’s absent but treated as empty by the underlying object model). (github.com)
  • If you need “only enforce when configured”, a common pattern is:
    • !has(self.namedCertificates) || self.namedCertificates.exists(c, /*match*/) (skip rule unless non-empty), because has() on a list means non-empty. (github.com)

Citations:


Guard namedCertificates and cert.names before dereferencing.

Lines 6005–6006 access two optional fields without has() guards: servingCerts.namedCertificates and cert.names. Both are optional in the schema—namedCertificates states "If no named certificates are provided..." and names is marked "optional list". Per Kubernetes CEL best practices, optional fields must be guarded with has() before dereferencing to prevent errors when fields are absent or empty.

Suggested fix
-                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
-                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
-                cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
+                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
+                && has(self.configuration.apiServer.servingCerts.namedCertificates)
+                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
+                has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
📝 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
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& has(self.configuration.apiServer.servingCerts.namedCertificates)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml`
around lines 6004 - 6006, The CEL expression dereferences optional fields
without guards—specifically
self.configuration.apiServer.servingCerts.namedCertificates and cert.names—so
update the predicate to wrap those accesses with has() checks (e.g.,
has(self.configuration.apiServer.servingCerts.namedCertificates) and
has(cert.names)) before calling .exists; adjust the inner exists() lambdas (the
one iterating namedCertificates and the cert.names.exists(...) call) to only run
when those has() checks pass so you don't access missing fields in the
namedCertificates and cert.names references.

Comment on lines +6355 to +6356
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Kubernetes CRD CEL validation rules: when should optional object/list fields be guarded with has(self.field), and are null or absent fields treated as absent during evaluation?

💡 Result:

When to guard optional object/list fields with has(self.field)

Guard any optional field when your rule would otherwise dereference it (access subfields) or call methods/macros on it (for lists: .size(), .all(), .exists(), indexing, etc.). The canonical pattern is to use short-circuiting:

  • Optional list: !has(self.items) || self.items.all(x, /* ... */)
  • Optional object: !has(self.cfg) || self.cfg.mode in ['A','B']

Kubernetes’ own examples use this style for optional fields (including optional lists) in CRD validation/transition rules. (kubernetes.io)

Are null and “absent” treated the same?

No.

  • Absent field: has(self.field) is false (field is unset). (kubernetes.io)
  • Field present with value null (only possible if the schema allows null): CEL sees a real null_type value. (kubernetes.io)
  • In CEL, null counts as “present” rather than “absent” (so has(self.field) can be true even if the value is null). (github.com)

So, if your schema permits null and you want “null behaves like unset”, explicitly guard both:

!has(self.field) || self.field == null || /* safe use of self.field */

(or equivalently: has(self.field) && self.field != null && ... when it must be provided).

Citations:


🏁 Script executed:

# First, check if the file exists and examine the context around lines 6355-6356
head -n 6360 api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml | tail -n 20

Repository: openshift/hypershift

Length of output: 1690


🏁 Script executed:

# Find the schema definition for namedCertificates to confirm it's optional
rg "namedCertificates" api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml -B 5 -A 5

Repository: openshift/hypershift

Length of output: 2361


🏁 Script executed:

# Check if namedCertificates is in any required array
rg "required:" api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml -A 20 | grep -i "namedCertificates\|servingCerts"

Repository: openshift/hypershift

Length of output: 46


🏁 Script executed:

# Find the schema definition for servingCerts and check its required array
rg "servingCerts:" api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml -A 50 | head -n 70

Repository: openshift/hypershift

Length of output: 3530


🏁 Script executed:

# Search more broadly for the required array that contains servingCerts properties
rg -A 100 "namedCertificates:" api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml | grep -A 30 "type:"

Repository: openshift/hypershift

Length of output: 5122


Guard namedCertificates with has() before calling .exists().

Line 6356 calls .exists() on self.configuration.apiServer.servingCerts.namedCertificates without first checking that the field exists. namedCertificates is an optional list field (not required), so a resource with servingCerts: {} will still hit the same validation failure this patch intended to fix. Kubernetes CEL validation rules require optional fields to be guarded with has(self.field) before dereferencing or calling methods on them.

Suggested fix
                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
+               && has(self.configuration.apiServer.servingCerts.namedCertificates)
                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
📝 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
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& has(self.configuration.apiServer.servingCerts.namedCertificates)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml`
around lines 6355 - 6356, The CEL expression calls .exists() on
self.configuration.apiServer.servingCerts.namedCertificates without first
checking the field exists; update the boolean guard by adding
has(self.configuration.apiServer.servingCerts.namedCertificates) before calling
.exists() so the expression becomes ... && has(self.configuration.apiServer) &&
has(self.configuration.apiServer.servingCerts) &&
has(self.configuration.apiServer.servingCerts.namedCertificates) &&
self.configuration.apiServer.servingCerts.namedCertificates.exists(...),
ensuring namedCertificates is present before dereferencing in the validation
rule.

Comment on lines 6365 to 6369
rule: '!self.services.exists(s, s.service == ''APIServer'' && has(s.servicePublishingStrategy.loadBalancer)
&& s.servicePublishingStrategy.loadBalancer.hostname != "" && has(self.configuration)
&& has(self.configuration.apiServer) && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
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.

⚠️ Potential issue | 🟠 Major

Guard the other optional fields in this CEL rule too.

This only fixes the servingCerts dereference. Line 6366 still reads the optional loadBalancer.hostname, and Lines 6368-6369 still read optional namedCertificates and cert.names. Valid specs such as servingCerts: {} or named certificates that rely on implicit names will still fail validation here instead of evaluating to false.

Suggested fix
-              rule: '!self.services.exists(s, s.service == ''APIServer'' && has(s.servicePublishingStrategy.loadBalancer)
-                && s.servicePublishingStrategy.loadBalancer.hostname != "" && has(self.configuration)
-                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
-                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
-                cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
+              rule: '!self.services.exists(s, s.service == ''APIServer'' && has(s.servicePublishingStrategy.loadBalancer)
+                && has(s.servicePublishingStrategy.loadBalancer.hostname)
+                && s.servicePublishingStrategy.loadBalancer.hostname != "" && has(self.configuration)
+                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
+                && has(self.configuration.apiServer.servingCerts.namedCertificates)
+                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
+                has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
📝 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
rule: '!self.services.exists(s, s.service == ''APIServer'' && has(s.servicePublishingStrategy.loadBalancer)
&& s.servicePublishingStrategy.loadBalancer.hostname != "" && has(self.configuration)
&& has(self.configuration.apiServer) && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
rule: '!self.services.exists(s, s.service == ''APIServer'' && has(s.servicePublishingStrategy.loadBalancer)
&& has(s.servicePublishingStrategy.loadBalancer.hostname)
&& s.servicePublishingStrategy.loadBalancer.hostname != "" && has(self.configuration)
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& has(self.configuration.apiServer.servingCerts.namedCertificates)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml`
around lines 6365 - 6369, The CEL validation rule currently dereferences
optional fields without guarding them; update the rule string so each optional
access is protected: add a
has(s.servicePublishingStrategy.loadBalancer.hostname) (or check hostname != ""
only after confirming has(loadBalancer) and has(hostname)), add
has(self.configuration.apiServer.servingCerts.namedCertificates) before calling
namedCertificates.exists(...), and guard cert.names with has(cert.names) (or
ensure cert.names exists/non-empty) before calling cert.names.exists; keep the
existing checks (e.g., has(self.configuration) and
has(self.configuration.apiServer) and
has(self.configuration.apiServer.servingCerts)) and apply these additional
has(...) checks around s.servicePublishingStrategy.loadBalancer.hostname,
self.configuration.apiServer.servingCerts.namedCertificates, and cert.names in
the CEL rule.

Comment on lines +6009 to +6010
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file exists and read the context around lines 6009-6010
file_path="api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml"

# Check file size first
wc -l "$file_path"

Repository: openshift/hypershift

Length of output: 215


🏁 Script executed:

# Read the context around lines 6009-6010 (lines 6000-6020 for context)
file_path="api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml"
sed -n '6000,6020p' "$file_path"

Repository: openshift/hypershift

Length of output: 1977


🏁 Script executed:

# Let's also look at the broader context to find this validation rule
file_path="api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml"
# Look at more context - the rule definition should be visible
sed -n '5995,6025p' "$file_path"

Repository: openshift/hypershift

Length of output: 2471


🏁 Script executed:

# Search for the schema definition of namedCertificates to confirm if it's optional
# First, let's find where the CRD schema is defined
rg -A 5 -B 5 "namedCertificates" api/hypershift/v1beta1/ --type yaml | head -100

Repository: openshift/hypershift

Length of output: 19415


🏁 Script executed:

# Let's also look for the servingCerts definition to understand the structure
rg -A 10 "servingCerts:" api/hypershift/v1beta1/ --type yaml | head -80

Repository: openshift/hypershift

Length of output: 15269


🌐 Web query:

Kubernetes CEL optional fields has() macro .exists() method documentation

💡 Result:

  • Kubernetes CEL (how to use has() for optional / dynamically-typed fields, and example using exists): Kubernetes “Common Expression Language in Kubernetes” reference. (kubernetes.io)

    • Shows has(self.expired) for checking presence before access, and an exists example like self.widgets.exists(w, ...). (kubernetes.io)
  • CEL language spec (what has() and exists() actually are): CEL spec “Language Definition → Macros”. (github.com)

    • has(e.f) is a macro for field/key presence checks (with detailed semantics for maps/proto2/proto3). (github.com)
    • e.exists(x, p) is a comprehension macro on lists (elements) or maps (keys), returning true if any element/key satisfies the predicate. (github.com)

Citations:


🌐 Web query:

CEL validation rule optional field dereference Kubernetes CRD schema

💡 Result:

In Kubernetes CRD CEL rules (x-kubernetes-validations), you must guard any dereference of an optional / possibly-absent field using the has() macro (and rely on && / || short-circuiting), otherwise the expression can fail when that field is unset.

Pattern (recommended):

!has(self.spec.optionalField) || self.spec.optionalField.subField == "x"

or

has(self.spec.optionalField) && self.spec.optionalField.subField == "x"

Kubernetes’ CEL docs explicitly show this style (for example: has(self.expired) && ...) and list has as a standard macro available for CRD CEL. [1] The Kubernetes blog post on CRD immutability also demonstrates guarding optional fields with has() (for example: !has(oldSelf.value) || has(self.value)). [2]

If you’re on Kubernetes v1.29+, Kubernetes also supports CEL optional types, but has()-guarding remains the portable approach across versions. [1]

Sources
[1] Kubernetes docs: “Common Expression Language (CEL) in Kubernetes” https://kubernetes.io/docs/reference/using-api/cel/
[2] Kubernetes blog: “Enforce CRD Immutability with CEL Transition Rules” https://kubernetes.io/blog/2022/09/29/enforce-immutability-using-cel/


Guard namedCertificates before calling .exists().

servingCerts is now protected, but namedCertificates is optional in the schema. If servingCerts is present while namedCertificates is omitted, the rule will attempt to dereference an absent field. Kubernetes CEL requires guarding optional field access with has() before dereferencing. Add has(self.configuration.apiServer.servingCerts.namedCertificates) before calling .exists().

Suggested fix
-                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
-                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
+                && has(self.configuration.apiServer)
+                && has(self.configuration.apiServer.servingCerts)
+                && has(self.configuration.apiServer.servingCerts.namedCertificates)
+                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
📝 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
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
&& has(self.configuration.apiServer)
&& has(self.configuration.apiServer.servingCerts)
&& has(self.configuration.apiServer.servingCerts.namedCertificates)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml`
around lines 6009 - 6010, The CEL expression dereferences an optional field:
before calling
self.configuration.apiServer.servingCerts.namedCertificates.exists(cert, ...)
you must guard namedCertificates with
has(self.configuration.apiServer.servingCerts.namedCertificates); update the
condition that currently checks has(self.configuration.apiServer) &&
has(self.configuration.apiServer.servingCerts) to also include
has(self.configuration.apiServer.servingCerts.namedCertificates) so the call to
.exists() on namedCertificates is only evaluated when that field is present.

Comment on lines +6538 to 6540
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and get basic info
file_path="api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml"
if [ -f "$file_path" ]; then
  echo "File found."
  wc -l "$file_path"
else
  echo "File not found. Searching for similar paths..."
  find . -name "OpenStack.yaml" -type f 2>/dev/null | head -20
fi

Repository: openshift/hypershift

Length of output: 197


🏁 Script executed:

# Read the specific lines mentioned in the review
file_path="api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml"
if [ -f "$file_path" ]; then
  echo "=== Lines 6530-6550 context ==="
  sed -n '6530,6550p' "$file_path"
fi

Repository: openshift/hypershift

Length of output: 1939


🏁 Script executed:

# Search for the CEL rule pattern to understand the validation rule
rg -A 10 -B 5 "servicePublishingStrategy.loadBalancer.hostname" . --type yaml 2>/dev/null | head -50

Repository: openshift/hypershift

Length of output: 10165


🏁 Script executed:

# Look for schema definitions related to servingCerts and namedCertificates
rg -A 5 "servingCerts" . --type yaml 2>/dev/null | grep -A 5 "namedCertificates" | head -40

Repository: openshift/hypershift

Length of output: 6385


🏁 Script executed:

# Search for the source schema or struct definition for servingCerts
find . -name "*.go" -type f -exec grep -l "namedCertificates" {} \; 2>/dev/null | head -20

Repository: openshift/hypershift

Length of output: 150


🏁 Script executed:

# Look for CRD source or schema files that define servingCerts structure
find . -name "*.go" -type f -exec grep -l "servingCerts" {} \; 2>/dev/null | head -20

Repository: openshift/hypershift

Length of output: 150


🏁 Script executed:

# Search for apiserver servingcerts structure definition
rg "type.*ServingCert" . --type go 2>/dev/null | head -20

Repository: openshift/hypershift

Length of output: 1660


🏁 Script executed:

# Read the APIServerServingCerts struct definition
cat -n ./api/vendor/github.com/openshift/api/config/v1/types_apiserver.go | grep -A 30 "type APIServerServingCerts struct"

Repository: openshift/hypershift

Length of output: 2216


🏁 Script executed:

# Also check the APIServerNamedServingCert struct
cat -n ./api/vendor/github.com/openshift/api/config/v1/types_apiserver.go | grep -A 15 "type APIServerNamedServingCert struct"

Repository: openshift/hypershift

Length of output: 1137


🌐 Web query:

Kubernetes CEL optional field access validation rules kubernetes.io

💡 Result:

To safely write Kubernetes CEL validation rules that touch optional (possibly absent) fields, use presence checks and/or CEL Optional types so the API server never evaluates a field access that isn’t valid.

1) Guard optional field access with has() + short-circuiting

Use the CEL has() macro to check presence, then rely on || / && short-circuiting so the field is only accessed when present. This pattern is shown in Kubernetes docs for validating optional parameters:

!has(params.optionalNumber) || (params.optionalNumber >= 5 && params.optionalNumber <= 10)

[1]

The Kubernetes CEL reference also calls out has() as the way to check accessibility before reading a (possibly missing) field (especially on dynamically-typed variables like object in admission policies). [2]

2) For CRD transition rules, compare presence across updates

When validating transitions with oldSelf, you can validate optional field “set/unset” transitions using:

has(self.field) == has(oldSelf.field)

(as documented in the CRD validation rules blog). [3]

3) If you need oldSelf during create / missing-old cases: optionalOldSelf

In CRD x-kubernetes-validations, setting optionalOldSelf: true makes oldSelf a CEL Optional so you can safely check and unwrap it:

  • oldSelf.hasValue() to test presence
  • oldSelf.value() to unwrap
    Example from the CRD docs:
self.foo == "foo" || (oldSelf.hasValue() && oldSelf.value().foo != "foo")

[4]

4) Know the version gate for CEL Optional Values

Kubernetes documents that CEL Optional Values were introduced in Kubernetes 1.29, and only API servers 1.29+ accept new writes of expressions using those optional features. [2]


Sources

[1] Kubernetes docs – Validating Admission Policy (optional parameters + has() + short-circuit)
[2] Kubernetes docs – Common Expression Language (CEL) in Kubernetes (has() guidance; Optional Values availability)
[3] Kubernetes blog – CRD Validation Rules (transition rule example has(self.field) == has(oldSelf.field))
[4] Kubernetes docs – Extend the Kubernetes API with CRDs (optionalOldSelf, oldSelf.hasValue(), oldSelf.value() examples)


Guard the remaining optional fields in this CEL rule.

The validation still dereferences two optional fields without presence checks: servingCerts.namedCertificates and cert.names. Both are defined as +optional in the schema, so the rule can fail on valid specs like servingCerts: {} or named certificate entries without explicit names. Kubernetes CEL guidance requires guarding optional field access with has() before dereferencing. (kubernetes.io)

Suggested fix
-                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
-                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
-                cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
+                && has(self.configuration.apiServer)
+                && has(self.configuration.apiServer.servingCerts)
+                && has(self.configuration.apiServer.servingCerts.namedCertificates)
+                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
+                has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
📝 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
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
&& has(self.configuration.apiServer)
&& has(self.configuration.apiServer.servingCerts)
&& has(self.configuration.apiServer.servingCerts.namedCertificates)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml`
around lines 6538 - 6540, The CEL rule dereferences two optional fields without
presence checks: self.configuration.apiServer.servingCerts.namedCertificates and
cert.names; update the expression used in the rule to guard those accesses with
has() checks (e.g., add
has(self.configuration.apiServer.servingCerts.namedCertificates) before
iterating namedCertificates and add has(cert.names) before calling
cert.names.exists) so the overall condition only evaluates those sub-expressions
when those optional fields are present.

Comment on lines +6698 to 6700
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Schema/source definitions for servingCerts and namedCertificates =="
rg -n -C3 'servingCerts:|namedCertificates:|json:".*namedCertificates,omitempty"|json:".*servingCerts,omitempty"' \
  api/hypershift/v1beta1 \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests

echo
echo "== All copies of the APIServer loadBalancer hostname validation =="
rg -n -C2 "APIServer loadBalancer hostname cannot be in ClusterConfiguration\\.apiserver\\.servingCerts\\.namedCertificates\\[\\]|namedCertificates\\.exists\\(cert" \
  api \
  cmd/install

Repository: openshift/hypershift

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if namedCertificates is optional in the schema (check required fields) =="
# Find servingCerts definitions and check if namedCertificates is in required list
rg -A 10 "servingCerts:" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | head -30

echo ""
echo "== Find the exact CEL validation rule for loadBalancer.hostname =="
rg -n "namedCertificates\.exists\(cert" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml

echo ""
echo "== Count total occurrences of this pattern in all generated manifests =="
rg "namedCertificates\.exists\(cert" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/ | wc -l

echo ""
echo "== List files with this pattern =="
rg -l "namedCertificates\.exists\(cert" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/

Repository: openshift/hypershift

Length of output: 1572


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Full context around the CEL rule (lines 6695-6705) =="
sed -n '6695,6705p' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml

echo ""
echo "== Check for 'required' list in servingCerts section =="
# Look for the required fields list near servingCerts definition
rg -B 5 -A 30 "servingCerts:" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | grep -A 20 "servingCerts:" | grep -E "(required:|namedCertificates)" | head -20

Repository: openshift/hypershift

Length of output: 1424


🏁 Script executed:

#!/bin/bash
set -euo pipefannat

echo "== Check all 3 files for the same pattern ==  "
for file in \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml; do
  echo "=== File: $(basename $file) ==="
  rg -B 3 "namedCertificates\.exists\(cert" "$file"
  echo ""
done

echo "== Verify namedCertificates is NOT in required list =="
# Search the actual schema for required fields under servingCerts
rg -A 50 '"servingCerts"' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | grep -A 40 'properties:' | head -50

Repository: openshift/hypershift

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check all 3 files for the same pattern ==  "
for file in \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml \
  cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml; do
  echo "=== File: $(basename $file) ==="
  rg -B 3 "namedCertificates\.exists\(cert" "$file"
  echo ""
done

echo "== Verify namedCertificates is NOT in required list =="
# Search the actual schema for required fields under servingCerts
rg -A 50 '"servingCerts"' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | grep -A 40 'properties:' | head -50

Repository: openshift/hypershift

Length of output: 1617


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the section defining servingCerts and check for required fields
echo "== Context: servingCerts schema definition with required list check =="
rg -n "servingCerts:" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | head -1

# Get line number and read 100 lines from that point
LINE=$(rg -n "servingCerts:" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | head -1 | cut -d: -f1)
START=$((LINE - 2))
END=$((LINE + 50))
echo "Lines $START to $END:"
sed -n "${START},${END}p" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml

echo ""
echo "== Check if namedCertificates field has minItems or is in required list =="
sed -n "${START},${END}p" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml | grep -E "required:|minItems:|namedCertificates"

Repository: openshift/hypershift

Length of output: 4048


Guard namedCertificates before calling .exists().

Line 6699 still dereferences an optional field. servingCerts is now checked, but namedCertificates is also optional in the schema (not in any required list), so a spec like configuration.apiServer.servingCerts: {} remains valid and can still fail CEL evaluation.

Proposed fix
-                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
-                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
+                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
+                && has(self.configuration.apiServer.servingCerts.namedCertificates)
+                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,

This same pattern appears in 3 manifest files and must be fixed in all of them:

  • hostedclusters-Hypershift-Default.crd.yaml
  • hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml
  • hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml`
around lines 6698 - 6700, The CEL expression dereferences an optional field:
guard self.configuration.apiServer.servingCerts.namedCertificates before calling
.exists(); update the predicate that currently uses
self.configuration.apiServer.servingCerts.namedCertificates.exists(cert, ...) to
first check has(self.configuration.apiServer.servingCerts.namedCertificates) &&
then call .exists(...). Apply the same change to the identical expressions in
hostedclusters-Hypershift-Default.crd.yaml,
hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml, and
hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml so all occurrences check
namedCertificates presence before .exists().

Comment on lines +7675 to 7677
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get the exact content at lines 7675-7677
wc -l cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml

Repository: openshift/hypershift

Length of output: 190


🏁 Script executed:

# Read the target lines with context
sed -n '7670,7680p' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml

Repository: openshift/hypershift

Length of output: 1094


🏁 Script executed:

# Find the schema definitions for servingCerts, namedCertificates, and names fields
# Search for where these are defined in the CRD
rg -n "servingCerts|namedCertificates" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml | head -30

Repository: openshift/hypershift

Length of output: 644


🏁 Script executed:

# Find the apiServer schema definition to check if servingCerts is optional
rg -n "apiServer:" cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml | head -20

Repository: openshift/hypershift

Length of output: 130


🏁 Script executed:

# Get context around servingCerts definition to check required/optional status
sed -n '555,590p' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml

Repository: openshift/hypershift

Length of output: 2574


🏁 Script executed:

# Search for namedCertificates item schema and names field definition
sed -n '560,620p' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml

Repository: openshift/hypershift

Length of output: 4091


🏁 Script executed:

# Check the required fields at each level to confirm optionality
# Looking at servingCerts definition
sed -n '555,610p' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml | grep -A 5 "required:"

Repository: openshift/hypershift

Length of output: 319


🏁 Script executed:

# Check if there's a required array for servingCerts or namedCertificates
sed -n '555,625p' cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml | cat -n

Repository: openshift/hypershift

Length of output: 5045


🌐 Web query:

Kubernetes CRD CEL validation .exists() on optional list fields

💡 Result:

In CRD x-kubernetes-validations CEL, an optional list field may be absent, and calling list macros like .exists() on an absent field will fail evaluation. Guard the call with has() (field presence check) or short-circuit around it. (kubernetes.io)

Common patterns:

  • If the list is optional (skip validation when missing):

    !has(self.spec.items) || self.spec.items.exists(x, x.name == "foo")
    
  • If the list must be present and satisfy a condition:

    has(self.spec.items) && self.spec.items.exists(x, x.name == "foo")
    
  • If you also need “non-empty”:

    has(self.spec.items) && size(self.spec.items) > 0 &&
    self.spec.items.exists(x, x.name == "foo")
    

.exists() itself is the right macro for “any element matches” once you’ve ensured the list value is present. (kubernetes.io)

Citations:


🌐 Web query:

Kubernetes CEL validation has() guard for optional array fields before exists()

💡 Result:

In Kubernetes CEL, guard an optional list/array field with has() and boolean short-circuiting before calling .exists(...):

!has(self.spec.myList) || self.spec.myList.exists(x, /* predicate using x */)

For nested optional parents, guard each level:

!has(self.spec) || !has(self.spec.myList) || self.spec.myList.exists(x, /* predicate */)

This works because Kubernetes CEL supports has() for field presence checks, and || short-circuits (so .exists() won’t be evaluated when the field is absent). It also supports list macros like .exists(...) on list fields. [1][2]

Sources:
[1] https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
[2] https://kubernetes.io/docs/reference/using-api/cel/


Guard optional list fields before calling .exists() to prevent CEL validation failures.

The rule at lines 7675-7677 guards servingCerts presence but omits guards for two optional list fields: namedCertificates and names. Since both are optional per the schema, a spec with an absent namedCertificates list or a namedCertificates item with an absent names field will cause CEL evaluation to fail, rejecting otherwise valid HostedCluster specs. Add has() checks before each .exists() call on these optional lists.

Suggested patch
-                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
-                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
-                cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
+                && has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
+                && has(self.configuration.apiServer.servingCerts.namedCertificates)
+                && self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
+                has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
📝 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
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
&& has(self.configuration.apiServer) && has(self.configuration.apiServer.servingCerts)
&& has(self.configuration.apiServer.servingCerts.namedCertificates)
&& self.configuration.apiServer.servingCerts.namedCertificates.exists(cert,
has(cert.names) && cert.names.exists(n, n == s.servicePublishingStrategy.loadBalancer.hostname)))'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml`
around lines 7675 - 7677, The CEL validation currently checks
self.configuration.apiServer.servingCerts but calls .exists() on two optional
lists without guarding them: servingCerts.namedCertificates.exists(...) and
cert.names.exists(...); update the rule to first check
has(self.configuration.apiServer.servingCerts.namedCertificates) before calling
namedCertificates.exists(...) and has(cert.names) before calling
cert.names.exists(...) so both optional lists are guarded and CEL evaluation
won't fail when those lists are absent.

@openshift-bot
Copy link
Copy Markdown

Stale PRs are closed after 21d of inactivity.

If this PR is still relevant, comment to refresh it or remove the stale label.
Mark the PR as fresh by commenting /remove-lifecycle stale.

If this PR is safe to close now please do so with /close.

/lifecycle stale

@openshift-ci openshift-ci Bot added lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels Apr 17, 2026
@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Apr 17, 2026

PR needs rebase.

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.

@openshift-bot
Copy link
Copy Markdown

Stale PRs rot after 14d of inactivity.

Mark the PR as fresh by commenting /remove-lifecycle rotten.
Rotten PRs close after an additional 7d of inactivity.

If this PR is safe to close now please do so with /close.

/lifecycle rotten
/remove-lifecycle stale

@openshift-ci openshift-ci Bot added lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. and removed lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. labels May 1, 2026
@hypershift-jira-solve-ci
Copy link
Copy Markdown

The analysis is complete. The root cause is clear from all evidence.

Test Failure Analysis Complete

Job Information

  • Prow Job: tide (merge gate)
  • Build ID: N/A (tide is a merge controller, not a test job)
  • PR: #7901 — OCPBUGS-77827 fix(api): add missing has() check for servingCerts in validation
  • Author: eponafyrefly
  • Branch: fix-servingcert-validation-regression
  • Created: 2026-03-10 (52 days ago)

Test Failure Analysis

Error

tide: Not mergeable. PR has a merge conflict.

Summary

No CI test jobs ran or failed. The sole reported failure is from tide — the Prow merge controller — which refuses to merge the PR because it has a git merge conflict with the current main branch. The PR was opened on March 10, 2026, and has not been rebased in 52 days. During that time, ~500+ commits have landed on main, and conflicting changes have been merged to the same files this PR touches (primarily api/hypershift/v1beta1/hostedcluster_types.go and the auto-generated CRD manifests). Additionally, the PR carries four blocking labels: needs-rebase, do-not-merge/work-in-progress, needs-ok-to-test, and lifecycle/rotten — any one of which would independently prevent merge. No presubmit test jobs (e2e-aws, e2e-aks, etc.) have been triggered because the PR has not received an ok-to-test label from an org member.

Root Cause

This is not a test failure — it is a merge conflict preventing tide from processing the PR.

The PR modifies api/hypershift/v1beta1/hostedcluster_types.go and 17 auto-generated CRD manifest YAML files. The PR was created 52 days ago (2026-03-10) with a single commit and has never been updated since. In that time, the main branch has received extensive changes to the same API types and generated CRD files (e.g., PRs #8265, #8343, #8360, #8387, #8393 merged in just the last 48 hours alone).

GitHub reports mergeable: false and mergeable_state: "dirty", confirming the branch cannot be cleanly merged.

The Prow bot has automatically applied the needs-rebase label, which is a blocking label that tide will not merge past. Three additional blocking labels are also present:

  • do-not-merge/work-in-progress — indicates the author marked this as WIP
  • needs-ok-to-test — no org member has approved test execution
  • lifecycle/rotten — the PR has been inactive long enough to be marked rotten
Recommendations
  1. Rebase the branch onto current main: git fetch upstream && git rebase upstream/main — resolve conflicts in hostedcluster_types.go and regenerate CRD manifests (make generate)
  2. Remove WIP status: If the fix is ready, remove the do-not-merge/work-in-progress label by updating the PR title (remove any "WIP" prefix) or commenting /remove-hold
  3. Get org approval: An openshift org member needs to comment /ok-to-test to allow CI presubmit jobs to run
  4. Revive the PR: Comment /lifecycle active to remove the lifecycle/rotten label
  5. Re-evaluate necessity: Given the PR is 52 days old, verify the original bug (OCPBUGS-77827) is still relevant and hasn't been fixed by another PR in the interim
Evidence
Evidence Detail
Tide status Not mergeable. PR has a merge conflict.
GitHub mergeable state mergeable: false, mergeable_state: "dirty", rebaseable: false
PR age Created 2026-03-10 (52 days ago), never updated
Blocking labels needs-rebase, do-not-merge/work-in-progress, needs-ok-to-test, lifecycle/rotten
CI test jobs None triggered — all presubmits show "Waiting for pipeline condition"
Conflicting files api/hypershift/v1beta1/hostedcluster_types.go + 16 generated CRD YAMLs
Recent main activity 5+ PRs merged in last 48h touching API/CRD files (e.g., #8265, #8343, #8360)

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

Labels

area/api Indicates the PR includes changes for the API area/cli Indicates the PR includes changes for CLI do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants