Skip to content

OCPSTRAT-3150: Add etcd sharding by resource kind support#8705

Open
jhjaggars wants to merge 8 commits into
openshift:mainfrom
jhjaggars:etcd-sharding
Open

OCPSTRAT-3150: Add etcd sharding by resource kind support#8705
jhjaggars wants to merge 8 commits into
openshift:mainfrom
jhjaggars:etcd-sharding

Conversation

@jhjaggars

@jhjaggars jhjaggars commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Implements etcd sharding by resource kind for HyperShift, as described in openshift/enhancements#1979.

Each etcd shard is registered as an independent ControlPlaneComponent using the CPO component framework. KAS is configured with --etcd-servers-overrides to route resources to the appropriate shard.

Key features:

  • Declarative shard configuration via spec.etcd.managed.shards and spec.etcd.unmanaged.shards
  • Per-shard storage (PersistentVolume or EmptyDir), replicas (1 or 3), and scheduling
  • Feature-gated behind EtcdSharding (TechPreviewNoUpgrade)
  • Full CEL validation: resource overlap prevention, immutability, DNS format validation
  • Per-shard TLS certificates, ServiceMonitors, PDBs
  • Both managed and unmanaged etcd support

API types aligned with enhancement proposal including:

  • EtcdShardResource with +listType=map composite keys
  • Per-field CEL immutability on shard specs
  • Explicit Replicas field (enum 1|3) per shard
  • Cross-shard resource overlap CEL validation

Summary by CodeRabbit

  • New Features

    • Etcd sharding: per-resource routing, per-shard storage, scheduling, per-shard control-plane components, per-shard TLS secrets, and feature gate.
    • Kube API server: per-shard etcd endpoint overrides and startup wait for shard DNS.
    • Etcd defrag: configurable leader-election ID.
  • Chores

    • Added local Dockerfile and updated .gitignore to ignore build outputs.
    • Broadened Prometheus etcd recording rules.
    • Component asset-directory override and related defaults/scheduling refinements.
  • Tests

    • Added unit tests for shard derivation and naming logic.

@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-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Jun 9, 2026
@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 9, 2026
@openshift-ci

openshift-ci Bot commented Jun 9, 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

@openshift-ci-robot

Copy link
Copy Markdown

@jhjaggars: This pull request explicitly references no jira issue.

Details

In response to this:

Implements etcd sharding by resource kind for HyperShift, as described in openshift/enhancements#1979.

Each etcd shard is registered as an independent ControlPlaneComponent using the CPO component framework. KAS is configured with --etcd-servers-overrides to route resources to the appropriate shard.

Key features:

  • Declarative shard configuration via spec.etcd.managed.shards and spec.etcd.unmanaged.shards
  • Per-shard storage (PersistentVolume or EmptyDir), replicas (1 or 3), and scheduling
  • Feature-gated behind EtcdSharding (TechPreviewNoUpgrade)
  • Full CEL validation: resource overlap prevention, immutability, DNS format validation
  • Per-shard TLS certificates, ServiceMonitors, PDBs
  • Both managed and unmanaged etcd support

API types aligned with enhancement proposal including:

  • EtcdShardResource with +listType=map composite keys
  • Per-field CEL immutability on shard specs
  • Explicit Replicas field (enum 1|3) per shard
  • Cross-shard resource overlap CEL validation

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.

@coderabbitai

coderabbitai Bot commented Jun 9, 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

This pull request implements EtcdSharding: a feature gate and manifest entry; new hostedcluster API types and CEL validations for managed/unmanaged shards; support package to compute effective shards and service names with tests; per-shard control-plane components (StatefulSet, Services, ServiceMonitor, PDB) and PKI helpers with controller reconciliation; KAS wiring for etcd-servers-overrides and wait-for-etcd updates; asset-directory builder and component changes; and ancillary Dockerfile, .gitignore, and Prometheus rule updates.

Sequence Diagram(s)

sequenceDiagram
  participant HostedController
  participant PKI
  participant ShardComponent
  participant EtcdStatefulSet
  participant KubeAPIServer
  HostedController->>PKI: ReconcileEtcdShardServerSecret(ns, shard)
  HostedController->>PKI: ReconcileEtcdShardPeerSecret(ns, shard)
  HostedController->>ShardComponent: NewShardComponent(shard) / apply manifests
  ShardComponent->>EtcdStatefulSet: adaptStatefulSetForShard (replicas, TLS, init/reset, storage, scheduling)
  KubeAPIServer->>EtcdStatefulSet: connect (via etcd-servers-overrides)
Loading

Suggested reviewers

  • sdminonne
🚥 Pre-merge checks | ✅ 10 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Topology-Aware Scheduling Compatibility ⚠️ Warning Shard Replicas field allows 1 or 3 without topology awareness. No validation prevents Replicas=3 on 2-node clusters; no anti-affinity prevents 3 pods pending on limited nodes. Add CEL validation to reject Replicas=3 on SingleReplica/DualReplica topologies, or require anti-affinity + appropriate PDB constraints.
✅ Passed checks (10 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 All 40 test cases use stable, deterministic names with no dynamic content. No UUIDs, timestamps, or generated identifiers present. All tests use standard Go testing, not Ginkgo.
Test Structure And Quality ✅ Passed PR tests use standard Go testing, not Ginkgo; all test files show good quality with table-driven tests, clear assertion messages, and repository pattern consistency.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed No Ginkgo e2e tests added in this PR. Three standard Go unit tests were added, not Ginkgo e2e tests, so the IPv6/disconnected network check does not apply.
No-Weak-Crypto ✅ Passed PR adds etcd sharding with TLS certificate generation but uses only standard Go crypto libraries and existing PKI helpers; no weak algorithms, custom crypto, or insecure comparisons detected.
Container-Privileges ✅ Passed PR does not introduce container privilege escalation: no privileged: true, hostPID/Network/IPC, SYS_ADMIN, or allowPrivilegeEscalation flags in any manifests.
No-Sensitive-Data-In-Logs ✅ Passed No explicit logging of sensitive data. EtcdServersOverrides contain internal hostnames/namespace but are not logged; only generic config errors logged.
Title check ✅ Passed The title clearly and specifically describes the main change: adding etcd sharding by resource kind support, which is the central feature implemented across the entire changeset.

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

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

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

@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 area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/documentation Indicates the PR includes changes for documentation area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release and removed do-not-merge/needs-area labels Jun 9, 2026
@openshift-ci

openshift-ci Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 18:07 Inactive
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.09091% with 153 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.63%. Comparing base (8795609) to head (c8199f4).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
...perator/controllers/hostedcontrolplane/pki/etcd.go 0.00% 23 Missing ⚠️
...ostedcontrolplane/hostedcontrolplane_controller.go 20.83% 17 Missing and 2 partials ⚠️
...controllers/hostedcontrolplane/v2/assets/assets.go 51.35% 12 Missing and 6 partials ⚠️
...or/controllers/hostedcontrolplane/v2/etcd/shard.go 92.34% 15 Missing and 2 partials ⚠️
support/controlplane-component/cronjob.go 0.00% 13 Missing ⚠️
support/controlplane-component/job.go 0.00% 13 Missing ⚠️
...t/controlplane-component/controlplane-component.go 38.88% 10 Missing and 1 partial ⚠️
support/controlplane-component/deployment.go 30.76% 9 Missing ⚠️
support/etcd/shards.go 89.28% 6 Missing and 3 partials ⚠️
support/controlplane-component/builder.go 0.00% 6 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8705      +/-   ##
==========================================
+ Coverage   43.45%   43.63%   +0.17%     
==========================================
  Files         771      773       +2     
  Lines       95718    96205     +487     
==========================================
+ Hits        41597    41975     +378     
- Misses      51234    51314      +80     
- Partials     2887     2916      +29     
Files with missing lines Coverage Δ
...or/controllers/hostedcontrolplane/v2/kas/params.go 90.00% <100.00%> (+1.20%) ⬆️
hypershift-operator/featuregate/feature.go 81.81% <100.00%> (+0.86%) ⬆️
support/controlplane-component/defaults.go 67.89% <100.00%> (ø)
support/controlplane-component/statefulset.go 66.66% <100.00%> (-3.93%) ⬇️
support/controlplane-component/status.go 72.61% <100.00%> (+0.32%) ⬆️
...ontrollers/hostedcontrolplane/v2/etcd/component.go 10.00% <0.00%> (-0.21%) ⬇️
etcd-defrag/main.go 0.00% <0.00%> (ø)
...or/controllers/hostedcontrolplane/v2/kas/config.go 88.94% <0.00%> (-0.73%) ⬇️
...or/controllers/hostedcontrolplane/manifests/pki.go 0.00% <0.00%> (ø)
...ontrollers/hostedcontrolplane/v2/kas/deployment.go 52.46% <0.00%> (+11.44%) ⬆️
... and 10 more

... and 1 file with indirect coverage changes

Flag Coverage Δ
cmd-support 37.22% <61.14%> (+0.09%) ⬆️
cpo-hostedcontrolplane 46.03% <73.13%> (+0.81%) ⬆️
cpo-other 45.10% <ø> (ø)
hypershift-operator 53.66% <100.00%> (+<0.01%) ⬆️
other 32.08% <0.00%> (-0.01%) ⬇️

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 62-64: The .gitignore entry "*.egg_build-amd64/" mismatches the
actual build directory pattern used by the Dockerfile (which uses
"_build-${TARGETARCH}/" -> "_build-amd64/"); update the ignore list so amd64
artifacts are ignored consistently by replacing or adding the pattern
"_build-amd64/" (alongside the existing "_build-arm64/" and "_build/") so all
architecture-specific build dirs match the Dockerfile copy source.

In `@Dockerfile.local`:
- Around line 1-18: The Dockerfile currently leaves the container running as
root (ENTRYPOINT ["/usr/bin/hypershift"]); add a non-root user and switch to it
with a USER instruction before ENTRYPOINT. Create/ensure a runtime user (e.g.,
adduser or groupadd/useradd or use a fixed UID like 1000), chown or adjust
permissions on the copied binaries in /usr/bin (hypershift, hcp,
hypershift-operator, karpenter-operator, control-plane-operator,
control-plane-pki-operator) so the non-root user can execute them, and then add
USER <non-root-user-or-uid> immediately before the ENTRYPOINT to comply with the
non-root requirement.
🪄 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: Enterprise

Run ID: 40924115-0d68-416c-9094-7fc72f13c8ed

📥 Commits

Reviewing files that changed from the base of the PR and between 832b848 and 7db3f5e.

⛔ Files ignored due to path filters (22)
  • api/hypershift/v1beta1/zz_generated.deepcopy.go is excluded by !**/zz_generated*.go, !**/zz_generated*
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests.yaml is excluded by !**/zz_generated*
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/EtcdSharding.yaml is excluded by !**/zz_generated.featuregated-crd-manifests/**
  • api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/EtcdSharding.yaml is excluded by !**/zz_generated.featuregated-crd-manifests/**
  • client/applyconfiguration/hypershift/v1beta1/etcdshardresource.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/etcdshardschedulingspec.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/managedetcdshardpersistentvolumespec.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/managedetcdshardspec.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/managedetcdshardstoragespec.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/managedetcdspec.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/unmanagedetcdshardspec.go is excluded by !client/**
  • client/applyconfiguration/hypershift/v1beta1/unmanagedetcdspec.go is excluded by !client/**
  • client/applyconfiguration/utils.go is excluded by !client/**
  • cmd/install/assets/crds/hypershift-operator/payload-manifests/featuregates/featureGate-Hypershift-TechPreviewNoUpgrade.yaml is excluded by !cmd/install/assets/**/*.yaml
  • cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml is excluded by !**/zz_generated.crd-manifests/**, !cmd/install/assets/**/*.yaml
  • cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml is excluded by !**/zz_generated.crd-manifests/**, !cmd/install/assets/**/*.yaml
  • cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml is excluded by !**/zz_generated.crd-manifests/**, !cmd/install/assets/**/*.yaml
  • cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml is excluded by !**/zz_generated.crd-manifests/**, !cmd/install/assets/**/*.yaml
  • docs/content/reference/aggregated-docs.md is excluded by !docs/content/reference/aggregated-docs.md
  • docs/content/reference/api.md is excluded by !docs/content/reference/api.md
  • vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go is excluded by !vendor/**, !**/vendor/**, !**/zz_generated*.go, !**/zz_generated*
📒 Files selected for processing (16)
  • .gitignore
  • Dockerfile.local
  • api/hypershift/v1beta1/featuregates/featureGate-Hypershift-TechPreviewNoUpgrade.yaml
  • api/hypershift/v1beta1/hostedcluster_types.go
  • control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go
  • control-plane-operator/controllers/hostedcontrolplane/pki/etcd.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/prometheus-recording-rules.yaml
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/shard.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/kas/config.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/kas/deployment.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/kas/params.go
  • support/controlplane-component/builder.go
  • support/controlplane-component/controlplane-component.go
  • support/controlplane-component/defaults.go
  • support/controlplane-component/status.go
  • support/etcd/shards.go

Comment thread .gitignore Outdated
Comment thread Dockerfile.local Outdated
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 19:26 Inactive

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

Caution

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

⚠️ Outside diff range comments (1)
control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go (1)

245-250: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Shard component registration is static and misses runtime shard spec updates.

registerComponents is only called during setup, so r.components is frozen from startup shard config. If spec.etcd.managed.shards changes later, new shard components won’t reconcile (and removed shards may continue) until CPO restarts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go`
around lines 245 - 250, registerComponents currently builds r.components once at
setup using hcp.Spec.Etcd.Managed.Shards, so runtime changes to
spec.etcd.managed.shards are ignored; instead, rebuild or extend the component
list at reconcile time. Update the logic so Reconcile (or the reconcile loop
that iterates r.components) computes the etcd shard components from the current
hcp.Spec.Etcd.Managed.Shards by calling etcdv2.NewShardComponent(shard) for each
shard and merging those into the component list used for that reconciliation
(rather than relying on the startup-populated r.components from
registerComponents); ensure you only mutate a local slice or replace
r.components atomically so removed shards stop being reconciled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go`:
- Around line 245-250: registerComponents currently builds r.components once at
setup using hcp.Spec.Etcd.Managed.Shards, so runtime changes to
spec.etcd.managed.shards are ignored; instead, rebuild or extend the component
list at reconcile time. Update the logic so Reconcile (or the reconcile loop
that iterates r.components) computes the etcd shard components from the current
hcp.Spec.Etcd.Managed.Shards by calling etcdv2.NewShardComponent(shard) for each
shard and merging those into the component list used for that reconciliation
(rather than relying on the startup-populated r.components from
registerComponents); ensure you only mutate a local slice or replace
r.components atomically so removed shards stop being reconciled.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: df4ec306-22e9-43bd-8a0c-4914e8ed2cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 7db3f5e and e0b7962.

⛔ Files ignored due to path filters (3)
  • api/hypershift/v1beta1/zz_generated.deepcopy.go is excluded by !**/zz_generated*.go, !**/zz_generated*
  • client/applyconfiguration/hypershift/v1beta1/unmanagedetcdshardspec.go is excluded by !client/**
  • cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/featuregated.hostedclusters.etcdsharding.testsuite.yaml is excluded by !cmd/install/assets/**/*.yaml
📒 Files selected for processing (12)
  • .gitignore
  • Dockerfile.local
  • api/hypershift/v1beta1/hostedcluster_types.go
  • control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go
  • control-plane-operator/controllers/hostedcontrolplane/manifests/pki.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/shard.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/kas/params.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/kas/params_test.go
  • hypershift-operator/featuregate/feature.go
  • support/etcd/shards.go
  • support/etcd/shards_test.go
💤 Files with no reviewable changes (1)
  • support/etcd/shards.go
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (3)
  • Dockerfile.local
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/shard.go
  • api/hypershift/v1beta1/hostedcluster_types.go

@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 20:04 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 20:27 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 20:49 Inactive

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

Caution

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

⚠️ Outside diff range comments (1)
control-plane-operator/controllers/hostedcontrolplane/v2/etcd/etcd_test.go (1)

76-136: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add test coverage for non-empty leaderElectionID.

The test only covers the empty leaderElectionID case. The new conditional logic at lines 169-171 in statefulset.go that appends --leader-election-id when non-empty is untested.

💚 Suggested additional test case
{
	name:      "When called with a leaderElectionID, it should pass the leader-election-id as an arg",
	namespace: "test-namespace",
	leaderElectionID: "etcd-shard-foo-defrag",
	validate: func(g Gomega, c corev1.Container) {
		g.Expect(c.Args).To(Equal([]string{
			"etcd-defrag-controller",
			"--namespace",
			"test-namespace",
			"--leader-election-id",
			"etcd-shard-foo-defrag",
		}))
	},
},

This requires adding leaderElectionID string to the test case struct and updating the call:

-			c := buildEtcdDefragControllerContainer(tc.namespace, "")
+			c := buildEtcdDefragControllerContainer(tc.namespace, tc.leaderElectionID)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@control-plane-operator/controllers/hostedcontrolplane/v2/etcd/etcd_test.go`
around lines 76 - 136, The test TestBuildEtcdDefragControllerContainer only
validates the empty leaderElectionID path; add a test case that supplies a
non-empty leaderElectionID and asserts the container args include
"--leader-election-id" and the provided ID. Update the test case struct in
TestBuildEtcdDefragControllerContainer to include leaderElectionID string, pass
that value into the call to buildEtcdDefragControllerContainer(namespace,
leaderElectionID), and add the suggested case (name: "...leaderElectionID...",
leaderElectionID: "etcd-shard-foo-defrag") that expects Args to equal
["etcd-defrag-controller","--namespace","test-namespace","--leader-election-id","etcd-shard-foo-defrag"]
so the conditional branch in statefulset.go is covered.
🧹 Nitpick comments (1)
control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go (1)

102-114: ⚡ Quick win

Extract duplicate scheduling logic into a shared helper.

This code duplicates adaptShardScheduling in shard.go:329-341 (from the relevant code snippets). Both apply NodeSelector and Tolerations identically to a StatefulSet pod template. Extract a shared helper to avoid divergence.

♻️ Suggested refactor

Create a shared helper (e.g., in this file or a common location):

func applySchedulingToStatefulSet(sts *appsv1.StatefulSet, nodeSelector map[string]string, tolerations []corev1.Toleration) {
	if len(nodeSelector) > 0 {
		if sts.Spec.Template.Spec.NodeSelector == nil {
			sts.Spec.Template.Spec.NodeSelector = map[string]string{}
		}
		for k, v := range nodeSelector {
			sts.Spec.Template.Spec.NodeSelector[k] = v
		}
	}
	if len(tolerations) > 0 {
		sts.Spec.Template.Spec.Tolerations = append(sts.Spec.Template.Spec.Tolerations, tolerations...)
	}
}

Then use it in both places:

-	if managedEtcdSpec != nil {
-		if len(managedEtcdSpec.Scheduling.NodeSelector) > 0 {
-			if sts.Spec.Template.Spec.NodeSelector == nil {
-				sts.Spec.Template.Spec.NodeSelector = map[string]string{}
-			}
-			for k, v := range managedEtcdSpec.Scheduling.NodeSelector {
-				sts.Spec.Template.Spec.NodeSelector[k] = v
-			}
-		}
-		if len(managedEtcdSpec.Scheduling.Tolerations) > 0 {
-			sts.Spec.Template.Spec.Tolerations = append(sts.Spec.Template.Spec.Tolerations, managedEtcdSpec.Scheduling.Tolerations...)
-		}
-	}
+	if managedEtcdSpec != nil {
+		applySchedulingToStatefulSet(sts, managedEtcdSpec.Scheduling.NodeSelector, managedEtcdSpec.Scheduling.Tolerations)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go`
around lines 102 - 114, The scheduling merge logic duplicated between the
StatefulSet block and adaptShardScheduling should be factored into a single
helper; add a function named applySchedulingToStatefulSet that accepts
(*appsv1.StatefulSet, nodeSelector map[string]string, tolerations
[]corev1.Toleration) and moves the NodeSelector merge and Tolerations append
logic into it, then replace the inline block in the StatefulSet creation (the
code handling managedEtcdSpec.Scheduling.NodeSelector/Tolerations) and the calls
in adaptShardScheduling to call applySchedulingToStatefulSet(sts,
managedEtcdSpec.Scheduling.NodeSelector, managedEtcdSpec.Scheduling.Tolerations)
so both sites reuse the same implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@control-plane-operator/controllers/hostedcontrolplane/v2/etcd/etcd_test.go`:
- Around line 76-136: The test TestBuildEtcdDefragControllerContainer only
validates the empty leaderElectionID path; add a test case that supplies a
non-empty leaderElectionID and asserts the container args include
"--leader-election-id" and the provided ID. Update the test case struct in
TestBuildEtcdDefragControllerContainer to include leaderElectionID string, pass
that value into the call to buildEtcdDefragControllerContainer(namespace,
leaderElectionID), and add the suggested case (name: "...leaderElectionID...",
leaderElectionID: "etcd-shard-foo-defrag") that expects Args to equal
["etcd-defrag-controller","--namespace","test-namespace","--leader-election-id","etcd-shard-foo-defrag"]
so the conditional branch in statefulset.go is covered.

---

Nitpick comments:
In
`@control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go`:
- Around line 102-114: The scheduling merge logic duplicated between the
StatefulSet block and adaptShardScheduling should be factored into a single
helper; add a function named applySchedulingToStatefulSet that accepts
(*appsv1.StatefulSet, nodeSelector map[string]string, tolerations
[]corev1.Toleration) and moves the NodeSelector merge and Tolerations append
logic into it, then replace the inline block in the StatefulSet creation (the
code handling managedEtcdSpec.Scheduling.NodeSelector/Tolerations) and the calls
in adaptShardScheduling to call applySchedulingToStatefulSet(sts,
managedEtcdSpec.Scheduling.NodeSelector, managedEtcdSpec.Scheduling.Tolerations)
so both sites reuse the same implementation.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Enterprise

Run ID: b94f399c-88b4-420f-8838-81a9b6394891

📥 Commits

Reviewing files that changed from the base of the PR and between e27c482 and 7b4aaf1.

📒 Files selected for processing (4)
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/etcd_test.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/shard.go
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go
  • etcd-defrag/main.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • control-plane-operator/controllers/hostedcontrolplane/v2/etcd/shard.go

@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 21:01 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 21:49 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 9, 2026 21:54 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 10, 2026 00:03 Inactive
@jhjaggars jhjaggars force-pushed the etcd-sharding branch 2 times, most recently from 188ab63 to 78d1b97 Compare June 10, 2026 01:17
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 10, 2026 01:22 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 10, 2026 12:47 Inactive
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 June 10, 2026 14:11 Inactive
@jhjaggars jhjaggars marked this pull request as ready for review June 10, 2026 14:18
@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 Jun 10, 2026
@openshift-ci openshift-ci Bot requested review from Nirshal and enxebre June 10, 2026 14:20

// ManagedEtcdSpec specifies the behavior of an etcd cluster managed by
// HyperShift.
// +openshift:validation:FeatureGateAwareXValidation:featureGate=EtcdSharding,rule="has(oldSelf.shards) == has(self.shards)",message="shards cannot be added or removed after creation"

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.

Is managedEtcdSpec a required field or otherwise latching? Is it an issue if someone removed the entire managedEtdcSpec and then re-added it with different config?

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.

+immutable is just a doc marker and does not enforce anything — good call, I will clean those up.

The actual immutability is enforced by CEL rules:

  1. managementType is immutable (L1880): rule="self == oldSelf" — so you cannot switch from Managed to Unmanaged.
  2. managed is required when type is Managed (L1873): rule="self.managementType == 'Managed' ? has(self.managed) : !has(self.managed)" — so you cannot remove the managed field while managementType remains Managed.
  3. shards presence is immutable (L1901): rule="has(oldSelf.shards) == has(self.shards)" — so you cannot add or remove the shards list after creation.

Together these mean: once created with Managed + shards, you can never remove managed and re-add it with different shard config.

}

// ManagedEtcdShardStorageType defines the type of storage for an etcd shard.
// +kubebuilder:validation:Enum=PersistentVolume;EmptyDir

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.

Is PersistentVolumeClaim more accurate here? Just wondering if that would align with the pod storage API better?

@jhjaggars jhjaggars Jul 1, 2026

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.

We are creating PVCs (via StatefulSet volumeClaimTemplates), not PVs directly. However, the existing etcd storage API already uses PersistentVolume as the enum value (L1951, ManagedEtcdStorageType), and the shard storage type was intentionally mirrored to stay consistent.

// +kubebuilder:validation:XValidation:rule="self.type == 'PersistentVolume' ? true : !has(self.persistentVolume)",message="persistentVolume is forbidden when type is not PersistentVolume"
type ManagedEtcdShardStorageSpec struct {
// type is the kind of storage implementation to use for this shard.
// PersistentVolume uses PVCs; EmptyDir uses ephemeral node storage (tmpfs).

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.

What does an end user need to know about using PVCs here. Expand this with explanation of the PersistentVolume field below and what happens when that is not configured

Generally these are structured

type is ...
Valid values are A, B, C.
When set to A ...

@jhjaggars jhjaggars Jul 1, 2026

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.

Expanded the godoc for both type and persistentVolume to follow the pattern you described. See the Type field comment on ManagedEtcdShardStorageSpec which now covers valid values, what each does, and inheritance behavior.

Type ManagedEtcdShardStorageType `json:"type,omitempty"`

// persistentVolume configures PVC-based storage for this shard.
// Only valid when type is PersistentVolume.

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.

Explain what happens when omitted but type is PersistentVolume

@jhjaggars jhjaggars Jul 1, 2026

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.

Addressed together with the type field docs above. The persistentVolume godoc now explains: "When omitted and type is PersistentVolume, the shard inherits the StorageClass and volume size from the parent spec.etcd.managed.storage.persistentVolume configuration."

Comment thread etcd-defrag/main.go
}

cmd.Flags().StringVar(&opts.Namespace, "namespace", os.Getenv("MY_NAMESPACE"), "The namespace this operator lives in (required)")
cmd.Flags().StringVar(&opts.LeaderElectionID, "leader-election-id", "etcd-defrag-controller-leader-elect", "The leader election lease name")

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.

Does the defrag controller care about sharding? Does it need to be passed a way to know which shard it's responsible for?

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.

No, the defrag controller itself is completely shard-unaware. It runs as a sidecar container inside each shard's StatefulSet pod, so it connects to localhost:2379 and naturally targets the colocated etcd. It discovers other members from there.

The only sharding accommodation is at the orchestration layer:

  1. Each shard gets a unique leader election lease name (etcd-defrag-{shardName}-leader-elect) so multiple defrag controllers don't compete for the same lease.
  2. Defrag is only deployed for 3-replica shards (replicas >= 3), since single-replica shards don't need coordinated defrag.

return func(_ component.WorkloadContext, pdb *policyv1.PodDisruptionBudget) error {
pdb.Name = shardName
pdb.Spec.Selector.MatchLabels["app"] = shardName
// For single-replica shards, allow voluntary eviction so node drains aren't blocked.

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.

Or don't deploy a PDB at all? Or is that more complex?

Is there any time where the single replica deployment temporarily has 2 replicas? Since this applies to a statefulset (right?) I'm assuming not

@jhjaggars jhjaggars Jul 1, 2026

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.

Since replicas is immutable (rule="self == oldSelf"), there is no lifecycle concern about needing to add a PDB later. Updated to skip the PDB entirely for single-replica shards via a predicate, rather than deploying a no-op one. I was thinking it might be worth discussing the availability behavior with single replica shards somewhere, but maybe it is self-evident for users of this feature.

})

// Enable defrag for multi-replica shards.
if replicas >= 3 && defragControllerPredicate(cpContext) {

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.

Does the storage backend come into this at all? Still useful on ephemeral storage?

@jhjaggars jhjaggars Jul 1, 2026

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.

Yes, defrag is still useful on ephemeral storage. Etcd defrag compacts the boltdb file to reclaim fragmented free pages. On tmpfs, the boltdb file lives in memory, so fragmented space is wasted RAM that counts against the container's cgroup memory limit. Defragging reclaims that memory directly.

The current gate is replicas >= 3 because defrag temporarily takes a member offline because you need quorum from the remaining members to survive. With a single replica, defrag would briefly make the shard unavailable. The defragControllerPredicate on the default etcd component gates on HighlyAvailable for the same reason (L68-70 in component.go).

So single-replica in-memory shards won't be defragged currently, though it might be worth allowing it, even with a temporary outage. I'd like to understand the real-world impact first though.

Comment on lines +311 to +313
// Memory limit must account for both the tmpfs (which counts against the
// container's cgroup) and etcd's own working-set memory. Without headroom
// the pod OOMs before the "disk" is full.

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.

Do we set memory limits on the etcd containers today? In OCP we don't set limits on any containers, not sure if that guidance transfers to HyperShift as well

@jhjaggars jhjaggars Jul 1, 2026

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.

The memory limit here is specific to the EmptyDir/tmpfs case. tmpfs usage counts against the container's cgroup memory, so without a limit the pod could consume unbounded memory and get OOM-killed unpredictably. The limit is set to tmpfs size + 512Mi headroom, with request=limit to get Guaranteed QoS for the memory dimension. This gives etcd a clear "disk full" error when the tmpfs fills up rather than a surprise OOM kill. It's different from what we normally do, but it feels necessary here.

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.

I agree, if using tmpfs with memory as the backing medium for the storage, being explicit on the memory request and limit will be more predictable.

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 1, 2026
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 1, 2026
@github-actions github-actions Bot temporarily deployed to docs-preview/pr-8705 July 1, 2026 19:11 Inactive
jhjaggars and others added 7 commits July 7, 2026 12:48
- Add EtcdSharding feature gate (TechPreviewNoUpgrade)
- Add EtcdShardResource, ManagedEtcdShardSpec, ManagedEtcdShardStorageSpec,
  EtcdShardSchedulingSpec, and UnmanagedEtcdShardSpec types
- Add Shards field to ManagedEtcdSpec and UnmanagedEtcdSpec
- Add CEL validation for shard resource routing (built-in types only)
- Regenerate deepcopy and featuregated CRD manifests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ndor

- Regenerate apply configurations for new etcd shard types
- Update CRD manifests for CustomNoUpgrade and TechPreviewNoUpgrade
- Add envtest suite for EtcdSharding feature gate validation
- Vendor updated API types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…POv2 framework

- Register EtcdSharding feature gate
- Add WithAssetDir() to component builder for shared asset directories
- Support etcd-prefixed components in priorityClass, DefaultReplicas,
  FSGroup, and checkDependencies
- Add EffectiveShards() helper to compute shard list from HCP spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add NewShardComponent() for shard etcd StatefulSet instances
- Add per-shard TLS certificate generation in PKI controller
- Add --etcd-servers-overrides KAS flag for shard routing
- Update wait-for-etcd to check all shard DNS entries
- Update prometheus recording rules to match all etcd shards
- Isolate etcd-defrag to skip shard members
- Register shard components conditionally in HCP reconciler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Document EtcdShardResource, ManagedEtcdShardSpec, and related types
- Document built-in-only routing limitation for shard resources

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Expand godocs for ManagedEtcdShardStorageSpec.Type and
  persistentVolume fields with valid values, behavior per value,
  and inheritance semantics
- Skip PDB for single-replica shards via predicate instead of
  deploying a no-op PDB (replicas is immutable so no lifecycle concern)
- Remove unused intstr import from shard.go
- Simplify PDB test to only cover multi-replica case
- Regenerate deepcopy, CRDs, clients, and vendor after rebase onto main
- Remove WithIdentityAdaptFunction calls removed upstream
- Update vendored deepcopy for SecretEncryptionStatus
Replace per-manifest adapt functions (adaptServiceForShard,
adaptDiscoveryServiceForShard, adaptPDBForShard) with Go template
rendering in asset YAMLs via WithTemplateData builder method.

- Add LoadManifestTemplated to assets package with missingkey=error
- Add WithTemplateData builder method to component framework
- Templatize etcd YAML assets with {{ .Name }}, {{ .ClientServiceName }},
  {{ .DiscoveryServiceName }}
- Wire template data through all manifest loading paths (update, delete,
  status)
- Remove imperative rename adapt functions from shard.go
- Keep adaptStatefulSetForShard for runtime domain logic
- Add comprehensive template rendering tests
@openshift-ci

openshift-ci Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

@hypershift-jira-solve-ci

hypershift-jira-solve-ci Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Now I have everything. The fix is to either:

  1. Add unhandledEnforcement: Warn to the crdify-config.yaml to downgrade all unhandled changes to warnings, or
  2. Register an xvalidation property comparator (which doesn't exist yet in crdify), or
  3. The XValidation changes need a specific handler.

The crdify tool's checkUnhandled function compares properties after all registered validators have consumed their fields. Since there is no registered validator that handles XValidations field changes, the catch-all checkUnhandled detects a diff and reports an error. The PR adds FeatureGateAwareXValidation markers that generate XValidations rules in the CRD, but the CRD schema checker has no way to categorize this as safe or unsafe.

Test Failure Analysis Complete

Job Information

Test Failure Analysis

Error

FAIL: CRD schema check failed with 4 error(s) across 14 CRD(s):

  ERROR: hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml: version v1beta1: ^.spec.etcd.unmanaged: unhandled: unhandled changes found
  ERROR: hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml: version v1beta1: ^.spec.etcd.managed: unhandled: unhandled changes found
  ERROR: hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml: version v1beta1: ^.spec.etcd.unmanaged: unhandled: unhandled changes found
  ERROR: hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml: version v1beta1: ^.spec.etcd.managed: unhandled: unhandled changes found

Error: breaking CRD schema changes detected
make: *** [Makefile:149: verify-crd-schema] Error 1

Summary

The verify-crd-schema step fails because the PR adds new XValidations (CEL validation rules) to the ManagedEtcdSpec and UnmanagedEtcdSpec types via +openshift:validation:FeatureGateAwareXValidation markers. These markers generate x-kubernetes-validations entries in the CRD schemas for HostedClusters and HostedControlPlanes. The crdify-based CRD schema checker used by HyperShift has no registered validator for XValidations changes, so its catch-all "unhandled changes" detector flags all four instances as breaking schema changes and errors out.

Root Cause

The CRD schema check tool (hack/tools/crd-schema-check) uses the sigs.k8s.io/crdify library to compare CRD schemas between the PR branch and origin/main. The tool works by:

  1. Flattening CRD schemas into per-property JSONSchemaProps objects
  2. Running registered property comparators (for enum, type, default, min/max, required, pattern, nullable, oneOf, description) — each comparator nil-outs the fields it handles
  3. After all comparators run, a catch-all checkUnhandled function checks if the old and new JSONSchemaProps are still unequal. Any remaining diff is reported as an "unhandled" error.

The PR adds the marker +openshift:validation:FeatureGateAwareXValidation:featureGate=EtcdSharding,rule="has(oldSelf.shards) == has(self.shards)" on both ManagedEtcdSpec and UnmanagedEtcdSpec. Code generation converts this into XValidations fields in the CRD schemas for the CustomNoUpgrade variants (the only variants checked by the tool). Since no registered crdify comparator handles XValidations, the field is never consumed, the checkUnhandled diff finds XValidations: nilXValidations: v1.ValidationRules{...}, and reports 4 errors (2 CRDs × 2 fields each: spec.etcd.managed and spec.etcd.unmanaged).

The crdify-config.yaml in the repo only configures an enum validation with additionPolicy: Allow. It does not set unhandledEnforcement (which defaults to Error), nor does it register any handler for XValidations changes — which is expected, since crdify itself has no built-in XValidations validator.

Recommendations
  1. Add unhandledEnforcement: Warn to hack/tools/crd-schema-check/crdify-config.yaml — This downgrades all unhandled changes (including XValidations) from errors to warnings, allowing the check to pass. This is the simplest fix but reduces the safety net for other unhandled changes. Example:

    unhandledEnforcement: Warn
    validations:
      - name: enum
        enforcement: Error
        configuration:
          additionPolicy: Allow
  2. Register a custom XValidation comparator — Extend the CRD schema check tool (hack/tools/crd-schema-check/main.go) to register a custom property comparator that handles XValidations field changes (nil-ing them out after comparing), so the catch-all detector doesn't flag them. This preserves strict checking for other unhandled changes.

  3. Use the Prow override — As suggested by the tool output, run /override verify-crd-schema on the Prow job if this is considered an intentional, safe API change. This is a short-term workaround only (this is a GitHub Actions job, not Prow, so the override may not apply directly).

Option 2 is the most robust long-term solution; option 1 is the fastest to unblock the PR.

Evidence
Evidence Detail
Failed make target make verify-crd-schema (Makefile:149)
Tool hack/tools/crd-schema-check using sigs.k8s.io/crdify library
Config hack/tools/crd-schema-check/crdify-config.yaml — only enum validator configured; unhandledEnforcement defaults to Error
CRD files flagged hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml, hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml
Schema paths ^.spec.etcd.managed and ^.spec.etcd.unmanaged in version v1beta1
Change type Addition of XValidations (CEL rules: has(oldSelf.shards) == has(self.shards)) — previously nil
Source markers +openshift:validation:FeatureGateAwareXValidation:featureGate=EtcdSharding on ManagedEtcdSpec and UnmanagedEtcdSpec in hostedcluster_types.go
crdify behavior checkUnhandled() in pkg/validations/compare.go — catch-all diff after all validators run; no validator consumes XValidations
Registry pkg/runner/registry.go — 18 validators registered; none for XValidations
Variant filtering Only CustomNoUpgrade variants checked (Default/TechPreviewNoUpgrade skipped by skipFeatureGateVariant)

// particular, resources backed by CustomResourceDefinitions and resources
// served by aggregated API servers (such as the openshift.io groups served
// by openshift-apiserver and oauth-apiserver) are NOT routed: entries for
// such resources are accepted but have no effect, and their data remains in

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.

I presume it's more complicated than it's worth to be able to validate this at admission time?

Comment on lines +311 to +313
// Memory limit must account for both the tmpfs (which counts against the
// container's cgroup) and etcd's own working-set memory. Without headroom
// the pod OOMs before the "disk" is full.

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.

I agree, if using tmpfs with memory as the backing medium for the storage, being explicit on the memory request and limit will be more predictable.

Scheduling EtcdShardSchedulingSpec `json:"scheduling,omitzero"`
}

// ManagedEtcdShardStorageType defines the type of storage for an etcd shard.

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.

I know we chatted about creating "types" of etcd shard, maybe like a EphemeralEtcd that then drove settings like in-memory storage etc.

I think what you have here gives us the toggles to design that as we see fit from a service provider perspective though 👍 and keeps HyperShift API from needing to have a strong opinion right now.

@joshbranham joshbranham 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.

Left a few previous comments for clarification, but overall, this approach should allow us to protect clusters from event abuse, which is a very common pattern.

We will need to adopt this config down the road in ROSA, and decide, for example, if we care about some loss of events if we added an event shard to all clusters. I don't think that needs to block this design implementation, however.

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 area/control-plane-operator Indicates the PR includes changes for the control plane operator - in an OCP release area/documentation Indicates the PR includes changes for documentation area/hypershift-operator Indicates the PR includes changes for the hypershift operator and API - outside an OCP release jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants