feat: add scheduling.zone.pinning - #344
Conversation
Signed-off-by: Joseph Heyburn <jdheyburn@gmail.com>
|
| // +kubebuilder:validation:items:MinLength=1 | ||
| // +kubebuilder:validation:items:MaxLength=63 |
There was a problem hiding this comment.
Zone pinning accepts invalid Kubernetes label values
zone.pinning.zones only enforces a 1–63-character length. Values such as eu-west-1a! therefore pass CRD validation and are copied unchanged into topology.kubernetes.io/zone node selectors. Kubernetes rejects ! in a label value when the resulting workload is created, leaving the affected ValkeyNode—and consequently cluster reconciliation—unable to become ready.
Require Kubernetes label-value syntax for each zone entry, regenerate the CRD, and add an admission regression case for an invalid value such as eu-west-1a!.
Artifacts
Targeted Go harness for invalid zone label value "eu-west-1a!"
- The exact executed harness checks the generated CRD bounds, renderer, pod template, and Kubernetes validator for "eu-west-1a!"; takeaway: the full reachable path is covered without production changes.
Successful targeted validation output for invalid label "eu-west-1a!"
- The executed Go test reports that "eu-west-1a!" is admitted by 1..63 bounds, rendered into both selectors, and rejected by Kubernetes label validation; takeaway: the defect is confirmed.
Envtest API-server bootstrap output while attempting invalid label "eu-west-1a!"
- The attempted envtest API admission run failed before specs ran because the bundled kube-apiserver did not become ready; takeaway: live API-server admission was environment-blocked, so the targeted direct-validator fallback was used.
Captured source for the invalid label "eu-west-1a!" harness
- This command capture records the copied exact harness source used to validate "eu-west-1a!"; takeaway: the executed test source is independently preserved.
This PR closes #343
Summary
Adds
spec.scheduling.zone.pinning, an ordered list of zones that assigns every pod a zone outright instead of asking the scheduler to balance across them. Phase 2 of #265, after the node axis (#310) and the zone axis (#340). Covers the zone placement half of #146.It's opt-in. A cluster that doesn't set it renders exactly as it did before, so upgrading the operator won't reschedule anything.
User docs are in
docs/valkeycluster.mdunder Scheduling.Features / Behaviour Changes
spec.scheduling.zone.pinning.zonestakes 1 to 32 unique zone names. A pod's zone iszones[(shardIndex + nodeIndex) % len(zones)], which lands on the pod as atopology.kubernetes.io/zonenodeSelector entry.Each shard starts at a different offset, so a shard's pods spread across consecutive zones. Primaries end up in separate zones too, as long as you have at least as many zones as shards.
Adding shards or replicas won't move existing pods, since their indices don't change.
Admission rejects duplicate or empty zone names, pinning combined with any non-
Disabledzone.spread, an in-place edit of the list, and a passthroughnodeSelectorthat sets the zone key itself.Implementation
Same idea as #310 and #340. The cluster controller renders the curated config into plain scheduling primitives on the ValkeyNode, and ValkeyNode itself doesn't change.
api/v1alpha1/valkeycluster_types.gogets theZonePinningtype and aPinningpointer onZoneScheduling, plus four schema bounds and four CEL rulesinternal/controller/scheduling.gogets three helpers sitting next to the existingeffectiveZoneSpread/zoneSpreadTSCs.buildClusterValkeyNodechanges by two lines.I picked
nodeSelectorovernodeAffinitysince we would have to insert the new condition into everynodeAffinityentry. It would simpler for me to picknodeSelectorsince that gets ANDed to everything - plus it is just one label.withZonePinreturns its input untouched when pinning is off, which keeps a nilnodeSelectornil.DeepEqualtreats nil and an empty map as different, so returning an empty map would roll every pod on upgrade and undo #317.Length bounds needed on
zonesfor the CEL cost estimator, I don't anticipate something needing 32 zones but it's some maximum limit. I am OK to lower this. MinLength=1 to validate that we're going to be adding a label value.Also updated the docs for
scheduling.affinityfield doc to what really happens (labels are ANDed together).Limitations
If you use persistence, pin at creation time
WaitForFirstConsumerStorageClassImmediatebinding the volume is provisioned before the scheduler picks a node, so pinning can't work at all, even on a fresh clusterThe zone list is immutable while set
Primaries only land in distinct zones while
shards <= len(zones)zones[shardIndex % len(zones)], and six shards over three zones puts 0 and 3 togetherTwo things admission can't see, so they aren't validated: a user
affinity, or a passthroughtopologySpreadConstraintson the zone key, that contradicts the pin. Both show up at runtime asDegradedwith reasonPodUnschedulable.Testing
Unit tests in
scheduling_test.goandvalkeynode_resources_test.go. Five new functions covering the round robin at every shard/node position in a 3x2x3 example, the single zone case, pinning off, nil preservation, that the caller's map isn't mutated, key precedence, and the rendered ValkeyNode with and without pinning.envtest in
scheduling_validation_test.go. 14 cases against a real API server, one per rule plus the accept cases, including remove-then-re-add, an omittedspreadblock to exercise the CEL fallback, and pinning alongsidenode.spreadto confirm the axes stay independent.make testpasses,make lintis clean, andmake manifests generateproduces no drift.No e2e in this one. It would need a multi zone cluster or relabelled nodes, and the rendering is a map assignment the unit tests already cover, with envtest exercising the validation against a real API server which was the same decision in #340.
Checklist
Before submitting the PR make sure the following are checked:
pre-commit run --all-filesor hooks on commit)