Skip to content

feat: stage pod template rolls via Spec.WorkloadRevision - #338

Merged
jdheyburn merged 10 commits into
valkey-io:mainfrom
daanvinken:feat/workload-roll-permit-gate
Aug 5, 2026
Merged

jdheyburn merged 10 commits into
valkey-io:mainfrom
daanvinken:feat/workload-roll-permit-gate

Conversation

@daanvinken

@daanvinken daanvinken commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

This PR closes #337

Summary

ValkeyCluster already stages ValkeyNode Spec updates carefully (one node at a time, replicas first, failover before primary). That care stopped at the ValkeyNode object: each node rewrote its single-pod StatefulSet/Deployment as soon as the computed pod template differed, so operator upgrades and other builder-only template changes could restart every pod at once.

This PR makes the authorized pod template a Spec field the cluster controller owns.

Features / Behaviour Changes

  • New ValkeyNode.spec.workloadRevision: hash of the fully built pod template, set by the ValkeyCluster controller.
  • Cluster-owned nodes apply a rolling template update only when the hash they compute matches spec.workloadRevision.
  • Template advances become normal Spec rolls (same one-at-a-time / replica-first / proactive failover path). A future rollingStrategy only needs to change how many Specs advance per reconcile.
  • First-time backfill of an empty workloadRevision (operator upgrade onto this feature) does not run proactive failover; it is bookkeeping only.
  • Standalone ValkeyNodes still apply template updates immediately.
  • Create path unchanged.
  • WorkloadRollPending / AwaitingWorkloadRevision when the node is waiting for Spec to catch up.
  • API-server probe defaults include period/timeout so user probe patches do not thrash templates.

Implementation

  • Shared builders compute the revision (computeWorkloadRevision / buildNodePodTemplate).
  • Cluster sets WorkloadRevision on desired Spec; scrape topology only when a failover-aware roll is needed.
  • Node gates rolling STS/Deployment applies on Spec match; still syncs non-template fields when the template already matches.

Limitations

  • No new user-facing roll strategy API yet.
  • Stuck-holder observability (slow roll vs jammed node) is follow-up.
  • Full e2e for staged template rolls is follow-up (tracking issue).

Testing

Checklist

  • This Pull Request is related to one issue.
  • Commit message explains what changed and why
  • Tests are added or updated.
  • Documentation files are updated.
  • I have run pre-commit locally (pre-commit run --all-files or hooks on commit)

Operator image upgrades that change the ValkeyNode pod template were
rewriting every 1-pod StatefulSet in one reconcile wave, taking the
whole cluster down at once.

Gate cluster-owned StatefulSet/Deployment template updates on
valkey.io/allow-workload-revision (granted one node at a time by the
ValkeyCluster controller, replicas first with proactive failover).
Surface WorkloadDrift while waiting. Own API-server defaults so
residual default noise does not re-trigger rolls. Spec and workload
grants share a single in-flight permit.

Standalone ValkeyNodes still apply immediately. Create path unchanged.

Signed-off-by: daanvinken <daanvinken@tythus.com>
CI Check formatting failed on third-party vs local import grouping.

Signed-off-by: daanvinken <daanvinken@tythus.com>
@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change introduces workload-revision authorization for cluster-owned ValkeyNode template updates, so pod changes can be rolled one node at a time with primary failover coordination. It also moves cluster TLS settings from spec.tls to spec.networking.tls.

An upgrade regression was reproduced: existing clusters stored with the previously served spec.tls.certificate.secretName lose their effective TLS settings after this change. The controller subsequently creates desired node workloads without the TLS secret mount and TLS environment configuration.

Confidence Score: 4/5

Not safe to merge until TLS configuration is preserved for existing clusters during upgrade.

The workload-roll behavior is covered by focused controller tests, but the reproduced API compatibility failure affects existing TLS-enabled installations and can remove their encryption configuration during reconciliation.

Files Needing Attention: api/v1alpha1/valkeycluster_types.go, config/crd/bases/valkey.io_valkeyclusters.yaml, internal/controller/valkeycluster_controller.go

Security Review

The TLS API move weakens upgrade safety for existing encrypted deployments. A stored cluster using the former TLS field decodes without an effective TLS configuration under the new API, which can remove TLS wiring from reconciled pods. This may downgrade encrypted client and replication traffic or prevent a workload configured for TLS from operating correctly. Preserve compatibility or migrate persisted objects before removing the legacy field.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for the posted P1 finding and referenced the reviewer comment with details.
  • A general-contract-validation-proof was added, showing exactly where ValkeyNode.Spec.TLS is assigned and how TLS pod wiring is guarded by the TLS-enabled flag, based on the repro Go source.
  • The P1 finding content was reviewed again; this proof has no artifacts attached.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (2)

  1. api/v1alpha1/valkeycluster_types.go, line 325 (link)

    P1 security Legacy TLS configuration is discarded

    Removing the already-served spec.tls field without a compatibility field, conversion, or migration means persisted TLS-enabled ValkeyCluster objects decode with no effective TLS configuration. GetTLS() only reads spec.networking.tls, so it returns nil for those clusters; the cluster reconciler then propagates nil TLS into each ValkeyNode, and the desired workload omits the TLS secret volume and VALKEY_TLS_* settings.

    Keep a deprecated legacy TLS field and make GetTLS() fall back to it with documented precedence, or provide a CRD conversion/migration that rewrites stored objects to spec.networking.tls before removing the old field. Add an upgrade regression test that verifies a persisted legacy TLS cluster retains TLS pod wiring after upgrade.

    Artifacts

    Standalone TLS upgrade reproduction source

    • The exact standalone Go executable decodes the legacy shape before and after the API move, reads the current CRD schema, and evaluates the current TLS accessor; it avoids controller TestMain and envtest bootstrap.

    Legacy TLS decoding before the field move

    • Executed `go run trex-artifacts/pr338-tls-upgrade-repro.go -mode before` from `/home/user/repo` and captured that the pre-move representation reads `legacy-cert`; the legacy field was effective.

    Current TLS decoding after the field move

    • Executed `go run trex-artifacts/pr338-tls-upgrade-repro.go -mode after` from `/home/user/repo` and captured that the current schema lacks `spec.tls`, `GetTLS()` is nil, and reconciler propagation is nil; existing TLS is lost.

    View artifacts

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 TLS configuration is dropped for pre-move v1alpha1 clusters

    • Bug
      • A stored cluster using the formerly served spec.tls.certificate.secretName shape is not represented by the current ValkeyClusterSpec, whose TLS accessor only examines spec.networking.tls. The executed repro decoded the same legacy object with the current type and observed GetTLS() == nil; it also read the installed CRD manifest and confirmed that spec.tls is absent.
    • Cause
      • ValkeyClusterSpec removed the legacy TLS field without a compatibility field, conversion, migration, or GetTLS() fallback. GetTLS() returns nil whenever Spec.Networking is nil.
    • Fix
      • Preserve backwards compatibility before releasing the same served version: retain a deprecated legacy spec.tls field and have GetTLS() fall back to it (with a documented precedence), or introduce a CRD conversion/migration path that rewrites all existing objects to spec.networking.tls before removing the legacy field. Add an upgrade regression test that stores a legacy TLS cluster, upgrades the CRD/controller, and asserts the resulting ValkeyNode and pod retain TLS wiring.

    T-Rex Ran code and verified through T-Rex

Reviews (9): Last reviewed commit: "Merge branch 'main' into feat/workload-r..." | Re-trigger Greptile

Comment thread internal/controller/valkeynode_controller.go Outdated
Drop unused anyNodeHasInFlightWorkloadRoll. Split reconcileValkeyNode
helpers so gocyclo stays under the limit. When the pod template already
matches, still sync labels, owner, and Spec so non-template controller
fields do not go stale.

Signed-off-by: daanvinken <daanvinken@tythus.com>

@jdheyburn jdheyburn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I did a quick scan on the review, but I don't know if I'm sold on the design yet. I would rather understand what is causing the ValkeyCluster controller to push the change out to so many nodes at once. I spent a bit of time on the original PRs to implement safe sequential rolls, so I'm interested to see what's happening.

Comment thread internal/controller/valkeynode_controller.go Outdated
@melancholictheory

Copy link
Copy Markdown
Contributor

this is the right extension. the sequencing the cluster does at the CR level was being undone the moment each node rewrote its own template, so gating the template apply behind the same permit closes a real gap. and it's good that maybeProactiveFailoverBeforeRoll fires for the workload path too, not just spec rolls: without that, staging alone would still restart a primary's pod as primary and drop writes. the failover-before-primary is the part that actually matters here, and you kept it.

it also composes well with #317: now that the operator owns the api-server defaults, the template only differs on genuine changes (image, real builder changes), so this stages the changes that should be staged rather than defaulting churn. the two together are the full fix for #337.

one thing to make sure is observable: the workload permit is a single cluster-wide token, so a node whose new pod never becomes Ready (bad image, failing probe) holds it indefinitely and no other node's workload roll proceeds. that's the safe behaviour, you don't want to cascade a broken rollout, but it means one stuck node wedges every other node's template roll silently. the AwaitingRollPermit condition helps, but it's worth distinguishing "waiting its turn" from "the holder is stuck" (the same updating-normally vs stuck-updating distinction from #267), so an operator can tell a slow rollout from a jammed one. a bounded wait or a stuck-holder event would do it.

(the DRY point on syncStatefulSetWithoutRoll seems right too, but that's cosmetics next to the above.)

@daanvinken

Copy link
Copy Markdown
Contributor Author

Let's take the discussion on the bug itself to #337 (comment)

Replace the allow-workload-revision annotation permit with a Spec field
owned by ValkeyCluster. The cluster sets WorkloadRevision to the hash of
the built pod template; the node applies rolling template updates only
when that hash matches. Operator upgrades and other builder drift become
normal one-at-a-time Spec rolls (replica-first, failover before primary).
Standalone nodes still apply immediately.

Signed-off-by: daanvinken <daanvinken@tythus.com>
anyNodeRequiresRoll must set desired Spec.WorkloadRevision the same way
reconcileValkeyNode does, or every settled node looks like it needs a
roll and the topology scrape runs every reconcile. Also default
Probe.TimeoutSeconds and PeriodSeconds so user probe patches do not
thrash the pod template.

Signed-off-by: daanvinken <daanvinken@tythus.com>

@daanvinken daanvinken left a comment

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.

E2E as a follow up I'd say.

(Meta) comment: A big chunk of the node-controller diff isn't new behavior, it's restructuring ensureStatefulSet/ensureDeployment and the cluster-controller extractions (maybeProactiveFailoverBeforeRoll, handleUnchangedValkeyNode).

We could split that out if this is found hard to review, let me know

I'll squash commits once this is approved.

// proactive failover still runs before killing a primary.
aclSecret, err := r.getClusterACLSecret(ctx, cluster)
if err != nil {
// Bootstrap: secret may not exist yet; hash without ACL annotations.

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.

When the ACL secret isn't ready the cluster hashes without template annotations, but the gate only engages when podTemplateWouldRoll is treu. On create the node bypasses the gate. It converges once the secret exists.

@jdheyburn

Copy link
Copy Markdown
Collaborator

@greptile-apps

Comment thread internal/controller/valkeycluster_controller.go Outdated

@jdheyburn jdheyburn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I did a local test and it worked great, so thank you! I had a couple of comments.

  • Can you update the PR title and description with the new setup?
  • Can you check up on some of the AI code review comments?
  • Can you check on the failing e2e test?
  • I agree that we can park an e2e test for this, but let's capture an issue for it so we don't lose track of it

Comment thread api/v1alpha1/valkeynode_types.go Outdated
Comment thread internal/controller/workload_roll.go Outdated
Do not run proactive failover when Spec only backfills an empty
WorkloadRevision (template unchanged). Scrape topology only when a
failover-aware roll is needed. Document WorkloadDrift, case StatefulSet
explicitly in buildNodePodTemplate, DRY non-template workload sync, and
scope the ACL-hash e2e pod lookups to the sample cluster.

Signed-off-by: daanvinken <daanvinken@tythus.com>
@daanvinken daanvinken changed the title feat: stage pod template rolls behind cluster permit feat: stage pod template rolls via Spec.WorkloadRevision Aug 4, 2026
@daanvinken

Copy link
Copy Markdown
Contributor Author

Thanks @jdheyburn , sorry was a bit rushed on Thursday.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds workload revision authorization and drift reporting. Controllers compute stable pod-template revisions, coordinate failover decisions, and defer cluster-owned workload rolls until revisions match. Probe defaults are normalized, and tests cover rollout gating.

Changes

Workload rollout coordination

Layer / File(s) Summary
Workload revision contract and hashing
api/v1alpha1/valkeynode_types.go, config/crd/bases/valkey.io_valkeynodes.yaml, internal/controller/workload_roll.go, internal/controller/workload_roll_test.go, docs/status-conditions.md
Adds the WorkloadRevision field, workload-roll condition constants, status documentation, stable pod-template hashing, ownership detection, revision computation, and authorization helpers.
Probe API defaults
internal/controller/valkeynode_resources.go, internal/controller/valkeynode_resources_test.go
Applies Kubernetes-equivalent defaults to liveness, readiness, and startup probes. Tests verify defaulting and preservation of explicit values.
Cluster roll and failover coordination
internal/controller/valkeycluster_controller.go, internal/controller/failover.go, internal/controller/failover_test.go
Uses ACL data, live workload-template hashes, and desired revisions for failover-aware roll decisions. Tests cover settled nodes, revision backfills, stale revisions, and image changes.
Node workload synchronization and drift gating
internal/controller/valkeynode_controller.go, internal/controller/valkeynode_controller_test.go
Explicitly creates and updates StatefulSets and Deployments. Cluster-owned nodes defer template changes until the authorized revision matches, record drift, requeue, and clear drift after authorization. Standalone nodes apply changes immediately.
Integration and E2E validation
internal/controller/valkeycluster_controller_test.go, test/e2e/valkeycluster_test.go
Updates reconciliation test calls for the expanded parameters. E2E ACL-hash checks filter pods by cluster label and inspect all matching pods.

Sequence Diagram(s)

sequenceDiagram
  participant ValkeyClusterController
  participant ACLSecret
  participant failover
  participant ValkeyNodeController
  participant StatefulSet
  ValkeyClusterController->>ACLSecret: read ACL data
  ValkeyClusterController->>ValkeyNodeController: pass ACL and live template snapshots
  ValkeyClusterController->>failover: compute desired revision and roll decision
  failover->>ValkeyClusterController: permit or defer reconciliation
  ValkeyNodeController->>ValkeyNodeController: compare authorized and desired revisions
  alt Revision matches
    ValkeyNodeController->>StatefulSet: apply template update
  else Revision mismatch
    ValkeyNodeController->>ValkeyNodeController: record drift and requeue
  end
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes staging pod template rolls through Spec.WorkloadRevision.
Description check ✅ Passed The description includes the required sections, issue reference, behavior changes, implementation, limitations, testing, and completed checklist.
Linked Issues check ✅ Passed The changes address issue #337 by staging workload template updates while preserving replica-first sequencing and failover before primary rolls.
Out of Scope Changes check ✅ Passed The probe-default handling, documentation, tests, and ACL-hash test updates support the staged workload rollout objective and are in scope.

Warning

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

🔧 golangci-lint (2.12.2)

Error: build linters: plugin(logcheck): plugin "logcheck" not found
The command is terminated due to an error: build linters: plugin(logcheck): plugin "logcheck" not found


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
internal/controller/valkeynode_controller_test.go (1)

677-698: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename ctrl, and add coverage for the standalone immediate-apply path.

Two points on this new test:

  1. ctrl := true shadows the conventional ctrl alias for sigs.k8s.io/controller-runtime. Rename it to isController, or use ptr.To(true).
  2. The gate has two branches. This test covers only the cluster-owned branch. gateRollingWorkloadUpdate returns true immediately when isClusterOwned(node) is false, and the PR states that standalone nodes apply template changes without waiting. Add a case that changes Spec.Image on a node with no controller owner reference and asserts that the StatefulSet template advances in the same reconcile and that no WorkloadDrift condition appears.

Consider also asserting result.RequeueAfter == 30 * time.Second on the deferred reconcile, so the backoff contract in Reconcile is pinned.

♻️ Proposed rename
-			ctrl := true
+			isController := true
@@
-				Controller: &ctrl,
+				Controller: &isController,
🤖 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 `@internal/controller/valkeynode_controller_test.go` around lines 677 - 698,
Rename the local ctrl boolean in the cluster-owned rolling-update test to
isController (or use the established pointer helper), then add coverage for the
standalone path: update Spec.Image on a ValkeyNode without a controller owner,
reconcile once, and assert the StatefulSet template advances immediately without
a WorkloadDrift condition. Also assert the deferred cluster-owned reconcile
returns a 30-second RequeueAfter.
internal/controller/valkeynode_controller.go (2)

444-449: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The "fresh read" uses the cache, and it mutates the caller's node.

Two points:

  1. r.Get reads through the manager cache, so it can still return the stale Spec.WorkloadRevision. The comment promises a fresh read. ValkeyNodeReconciler already holds APIReader for uncached reads. Either use APIReader here or soften the comment, because the current code relies on the watch plus the 30-second requeue to converge.
  2. node.Spec = fresh.Spec mutates the caller's object inside a function that reads as a pure predicate. desired was already built from the previous spec, so node.Spec and desired disagree after this line. No current caller depends on the mutation, so removing it makes the contract clearer.
♻️ Proposed change
-	// Fresh read: cluster may have advanced Spec.WorkloadRevision after this reconcile started.
+	// Uncached read: the cluster may have advanced Spec.WorkloadRevision after
+	// this reconcile started and the cache may not have observed it yet.
 	fresh := &valkeyiov1alpha1.ValkeyNode{}
-	if err := r.Get(ctx, client.ObjectKeyFromObject(node), fresh); err != nil {
+	if err := r.APIReader.Get(ctx, client.ObjectKeyFromObject(node), fresh); err != nil {
 		return false, err
 	}
-	node.Spec = fresh.Spec
 	if workloadRevisionAllows(fresh, desiredHash) {

APIReader is nil in the unit tests in internal/controller/valkeynode_controller_test.go, so set it when you adopt this change.

🤖 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 `@internal/controller/valkeynode_controller.go` around lines 444 - 449, Update
the fresh-read logic in the relevant ValkeyNodeReconciler predicate to use
APIReader for an uncached read, and remove the node.Spec = fresh.Spec mutation
so the predicate does not alter its caller. Ensure APIReader is initialized in
the unit-test setup where it is currently nil, while preserving the existing
error return and desired-state evaluation.

528-546: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify the signature, and note that workload annotations are never synced.

The helper takes seven parameters, two of them any, while it already receives obj client.Object. Labels and owner references are readable from obj, and the kind is available from the object type. Passing the specs as any removes compile-time type checking; a future call site that swaps a StatefulSetSpec for a DeploymentSpec would compile and always report "changed".

Separately, neither this helper nor the two callers copy desired.Annotations onto the live object. If buildValkeyNodeStatefulSet or buildValkeyNodeDeployment sets object-level annotations, those annotations are applied only at create time and never reconciled afterwards.

🤖 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 `@internal/controller/valkeynode_controller.go` around lines 528 - 546,
Refactor the maybeUpdateWorkloadWithoutRoll function signature to remove
redundant parameters (beforeLabels, afterLabels, beforeOwners, afterOwners,
kind, name) by extracting labels and owner references directly from the obj
parameter and deriving the kind from obj's type. Replace the `any` types for
beforeSpec and afterSpec with properly typed specs to restore compile-time type
safety in the comparison logic. Additionally, add annotation synchronization to
copy desired annotations onto the live object during the update, ensuring
annotations set by buildValkeyNodeStatefulSet or buildValkeyNodeDeployment are
reconciled on every update and not just at creation time.
🤖 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 `@internal/controller/valkeycluster_controller.go`:
- Around line 546-552: Update
internal/controller/valkeycluster_controller.go#L546-L552 in
reconcileValkeyNodes to preserve the successful ACL Secret snapshot, continue
treating only NotFound as absent, and return any other getClusterACLSecret
error. Update internal/controller/valkeycluster_controller.go#L620-L626 to pass
that snapshot into reconcileValkeyNode and use it for WorkloadRevision
calculation instead of performing a second Secret read.
- Around line 694-699: When the proactiveFailover call fails or times out at the
error handling block around lines 694-699, change the return value from false to
true. This will cause the controller to requeue before proceeding with the
workload/template update (which would otherwise be authorized by the false
return at CreateOrUpdate), ensuring the primary is not rolled immediately after
a failed proactive failover.

In `@internal/controller/valkeynode_controller.go`:
- Around line 150-159: Reorder the reconciliation flow in
valkeynode_controller.go so that the call to applyLiveConfig (and any clearing
of stale conditions) executes before the workload drift check using
meta.IsStatusConditionTrue and
valkeyiov1alpha1.ValkeyNodeConditionWorkloadDrift. Move the early return with
RequeueAfter to occur after the live config application so that the status
update propagates before requeuing, allowing drifted nodes waiting for
Spec.WorkloadRevision to unblock cluster progression by first applying live
config.
- Around line 422-431: The getACLSecret function does not validate that the
clusterName parameter is non-empty before constructing the internal secret name
via getInternalSecretName, causing silent failures when the valkey.io/cluster
label is missing. Add a validation check at the start of getACLSecret to return
an error with a clear message if clusterName is empty, allowing the controller
to fail fast instead of continuing through the reconciliation path.

---

Nitpick comments:
In `@internal/controller/valkeynode_controller_test.go`:
- Around line 677-698: Rename the local ctrl boolean in the cluster-owned
rolling-update test to isController (or use the established pointer helper),
then add coverage for the standalone path: update Spec.Image on a ValkeyNode
without a controller owner, reconcile once, and assert the StatefulSet template
advances immediately without a WorkloadDrift condition. Also assert the deferred
cluster-owned reconcile returns a 30-second RequeueAfter.

In `@internal/controller/valkeynode_controller.go`:
- Around line 444-449: Update the fresh-read logic in the relevant
ValkeyNodeReconciler predicate to use APIReader for an uncached read, and remove
the node.Spec = fresh.Spec mutation so the predicate does not alter its caller.
Ensure APIReader is initialized in the unit-test setup where it is currently
nil, while preserving the existing error return and desired-state evaluation.
- Around line 528-546: Refactor the maybeUpdateWorkloadWithoutRoll function
signature to remove redundant parameters (beforeLabels, afterLabels,
beforeOwners, afterOwners, kind, name) by extracting labels and owner references
directly from the obj parameter and deriving the kind from obj's type. Replace
the `any` types for beforeSpec and afterSpec with properly typed specs to
restore compile-time type safety in the comparison logic. Additionally, add
annotation synchronization to copy desired annotations onto the live object
during the update, ensuring annotations set by buildValkeyNodeStatefulSet or
buildValkeyNodeDeployment are reconciled on every update and not just at
creation time.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a6a1513a-c8af-4320-a563-c02db82839a9

📥 Commits

Reviewing files that changed from the base of the PR and between 1c64d35 and 2f3e26a.

📒 Files selected for processing (13)
  • api/v1alpha1/valkeynode_types.go
  • config/crd/bases/valkey.io_valkeynodes.yaml
  • docs/status-conditions.md
  • internal/controller/failover.go
  • internal/controller/failover_test.go
  • internal/controller/valkeycluster_controller.go
  • internal/controller/valkeynode_controller.go
  • internal/controller/valkeynode_controller_test.go
  • internal/controller/valkeynode_resources.go
  • internal/controller/valkeynode_resources_test.go
  • internal/controller/workload_roll.go
  • internal/controller/workload_roll_test.go
  • test/e2e/valkeycluster_test.go

Comment thread internal/controller/valkeycluster_controller.go Outdated
Comment thread internal/controller/valkeycluster_controller.go
Comment thread internal/controller/valkeynode_controller.go Outdated
Comment thread internal/controller/valkeynode_controller.go
Comment thread internal/controller/failover.go Outdated
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

Use live StatefulSet/Deployment template hashes so empty WorkloadRevision
backfill skips proactive failover only when the live template already
matches. Share one ACL secret snapshot for preflight and Spec updates.
Defer Spec rolls when proactive failover fails. Apply live config before
WorkloadDrift requeue so drifted nodes do not block on stale LiveConfig.

Signed-off-by: daanvinken <daanvinken@tythus.com>

@bjosv bjosv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR looks great to me, just a comment about the condition.
(there is also something with ACL-roll in the e2e, I have no clue yet though)

Comment thread api/v1alpha1/valkeynode_types.go Outdated
Drift reads as an error; this is expected staging while Spec.WorkloadRevision
catches up. Reason stays AwaitingWorkloadRevision.

Signed-off-by: daanvinken <daanvinken@tythus.com>
Spec.WorkloadRevision applies ACL template updates one node at a time, so
items[0] may keep the old hash while another node has already rolled.
Assert at least one cluster pod shows a new internal-acl-hash.

Signed-off-by: daanvinken <daanvinken@tythus.com>
@daanvinken

daanvinken commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

About the E2E; creates a Valkey Cluster deployment (end of the happy path):

  1. Read pod annotation valkey.io/internal-acl-hash
  2. Delete internal-…-system-passwords
  3. Secret comes back, operator logs fallback
  4. Wait up to 5 minutes for that hash on items[0] to change
    → times out: hash never changes

Before WorkloadRevision, the node controller rewrote every STS template as soon as the ACL secret hash changed. Pods all rolled; the assertion passed.

Now an ACL hash change is a template change. Apply waits on Spec.WorkloadRevision, which the cluster advances one node at a time, with failover-aware handling when live template ≠ authorized hash.

Fixing by keeping the secret recreated + fallback log checks. Drop the assumption of an immediate flip on a single arbitrary pod (items[0]). Assert that ACL hash progresses for the cluster (any or all pods under the cluster
label) under staged rolls.

@daanvinken

Copy link
Copy Markdown
Contributor Author

@jdheyburn @bjosv I think we're good here now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (2)
test/e2e/valkeycluster_test.go (2)

1647-1649: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Check the existing zone label before mutation.

The lookup error is discarded. If this lookup fails but the later label command succeeds, cleanup treats the prior value as empty and removes an existing zone label. Fail before changing the node label when the lookup fails.

Proposed fix
-				original, _ := utils.Run(exec.Command("kubectl", "get", "node", w,
+				original, err := utils.Run(exec.Command("kubectl", "get", "node", w,
 					"-o", "jsonpath={.metadata.labels['topology.kubernetes.io/zone']}"))
+				Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to get zone label for node %s", w))
 				original = strings.TrimSpace(original)
🤖 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 `@test/e2e/valkeycluster_test.go` around lines 1647 - 1649, Handle the error
returned by utils.Run when retrieving the node’s existing zone label in the
mutation setup around the kubectl lookup. Abort and return the error before
executing any label-changing command if the lookup fails, while preserving the
trimmed original label value on success.

605-619: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-532): Insertion of Sensitive Information into Log File

Reachability: Internal · Exploitability: Moderate

Do not log the decoded operator password in command arguments.

operatorPassword is decoded from a Secret and passed into commands that utils.Run logs via GinkgoWriter, so the password is exposed in e2e run output. Use secret-backed client-pod configuration, environment injection inside the client pod, or avoid logging the full command arguments.

  • test/e2e/valkeycluster_test.go#L495-550: remove -a operatorPassword from the initial valkey-cli ACL LIST command.
  • test/e2e/valkeycluster_test.go#L605-619: remove operatorPassword from the sh -c argument.
  • test/e2e/valkeycluster_test.go#L671-676: remove operatorPassword from the sh -c argument.
🤖 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 `@test/e2e/valkeycluster_test.go` around lines 605 - 619, Stop exposing the
decoded operator password through logged command arguments in the Valkey e2e
tests: remove the -a operatorPassword usage from the initial ACL LIST command
and remove operatorPassword from the sh -c arguments in the command blocks at
test/e2e/valkeycluster_test.go lines 605-619 and 671-676. Use secret-backed
client-pod configuration or environment injection so authentication still works
without logging the password.
🤖 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 `@test/e2e/valkeycluster_test.go`:
- Around line 1647-1649: Handle the error returned by utils.Run when retrieving
the node’s existing zone label in the mutation setup around the kubectl lookup.
Abort and return the error before executing any label-changing command if the
lookup fails, while preserving the trimmed original label value on success.
- Around line 605-619: Stop exposing the decoded operator password through
logged command arguments in the Valkey e2e tests: remove the -a operatorPassword
usage from the initial ACL LIST command and remove operatorPassword from the sh
-c arguments in the command blocks at test/e2e/valkeycluster_test.go lines
605-619 and 671-676. Use secret-backed client-pod configuration or environment
injection so authentication still works without logging the password.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ff33d4e-7853-4c30-ac8a-91506f8910a5

📥 Commits

Reviewing files that changed from the base of the PR and between d1d1cb6 and 0a225c5.

📒 Files selected for processing (4)
  • config/crd/bases/valkey.io_valkeynodes.yaml
  • internal/controller/valkeycluster_controller.go
  • internal/controller/valkeynode_resources_test.go
  • test/e2e/valkeycluster_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/controller/valkeycluster_controller.go
  • internal/controller/valkeynode_resources_test.go
  • config/crd/bases/valkey.io_valkeynodes.yaml

@bjosv bjosv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM and works fine in manual tests (like upgrading the operator with new default)

@jdheyburn jdheyburn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great thank you!

@jdheyburn
jdheyburn merged commit 709bf53 into valkey-io:main Aug 5, 2026
10 checks passed
melancholictheory added a commit to melancholictheory/valkey-operator that referenced this pull request Aug 5, 2026
ACL was carried in the pod template annotations, so an ACL edit changed
Spec.WorkloadRevision and rolled every node one at a time (valkey-io#338). The
operator can reload the ACL on a running server with ACL LOAD, so a roll
is unnecessary: this takes ACL out of the WorkloadRevision hash and
reloads the mounted aclfile live instead.

- buildPodTemplateAnnotations no longer stamps the ACL hash, so ACL edits
  do not enter the workload revision and never roll a pod.
- the ValkeyNode controller reloads the mounted aclfile (ACL LOAD) on
  reconcile and watches the internal ACL Secret, so an edit is picked up
  promptly rather than on the next resync. It reports an ACLApplied
  condition once the desired user set and password hashes are observably
  live.
- the operator user gains +acl|load, +acl|getuser and +acl|users.
- drop the now-dead ACL-secret threading through the cluster controller
  and the failover roll preflight.

Recovering an operator locked out by a deleted password Secret needs a
staged recovery through the cluster controller and is left as a follow-up.
melancholictheory added a commit to melancholictheory/valkey-operator that referenced this pull request Aug 5, 2026
ACL was carried in the pod template annotations, so an ACL edit changed
Spec.WorkloadRevision and rolled every node one at a time (valkey-io#338). The
operator can reload the ACL on a running server with ACL LOAD, so a roll
is unnecessary: this takes ACL out of the WorkloadRevision hash and
reloads the mounted aclfile live instead.

- buildPodTemplateAnnotations no longer stamps the ACL hash, so ACL edits
  do not enter the workload revision and never roll a pod.
- the ValkeyNode controller reloads the mounted aclfile (ACL LOAD) on
  reconcile and watches the internal ACL Secret, so an edit is picked up
  promptly rather than on the next resync. It reports an ACLApplied
  condition once the desired user set and password hashes are observably
  live.
- the operator user gains +acl|load, +acl|getuser and +acl|users.
- drop the now-dead ACL-secret threading through the cluster controller
  and the failover roll preflight.

Recovering an operator locked out by a deleted password Secret needs a
staged recovery through the cluster controller and is left as a follow-up.

Signed-off-by: melancholictheory <selimvhorst@gmail.com>
melancholictheory added a commit to melancholictheory/valkey-operator that referenced this pull request Aug 7, 2026
ACL was carried in the pod template annotations, so an ACL edit changed
Spec.WorkloadRevision and rolled every node one at a time (valkey-io#338). The
operator can reload the ACL on a running server with ACL LOAD, so a roll
is unnecessary: this takes ACL out of the WorkloadRevision hash and
reloads the mounted aclfile live instead.

- buildPodTemplateAnnotations no longer stamps the ACL hash, so ACL edits
  do not enter the workload revision and never roll a pod.
- the ValkeyNode controller reloads the mounted aclfile (ACL LOAD) on
  reconcile and watches the internal ACL Secret, so an edit is picked up
  promptly rather than on the next resync. It reports an ACLApplied
  condition once the desired user set and password hashes are observably
  live.
- the operator user gains +acl|load, +acl|getuser and +acl|users.
- drop the now-dead ACL-secret threading through the cluster controller
  and the failover roll preflight.

Recovering an operator locked out by a deleted password Secret needs a
staged recovery through the cluster controller and is left as a follow-up.

Signed-off-by: melancholictheory <selimvhorst@gmail.com>
jdheyburn added a commit that referenced this pull request Aug 18, 2026
<!--
Thanks for contributing to Valkey Operator!

Please make sure you are aware of our contributing guidelines [available

here](https://github.com/valkey-io/valkey-operator/blob/main/CONTRIBUTING.md)

-->

This PR closes #264 

### Summary

This is a refactor to remove `serverConfigHash` from ValkeyNode.

Since #338 introduced a `WorkloadRevision` to ValkeyNode, there is
duplicated functionality which means we no longer need a
`serverConfigHash`.

### Features / Behaviour Changes

- ValkeyNode.spec.serverConfigHash is removed from the CRD (internal,
operator-managed field; nothing outside the operator reads it).
- No runtime behaviour changes: config-change rolls, live-config apply
(CONFIG SET), and ACL live-apply are unchanged. A frozen pin test
(internal/controller/config_rollhash_test.go) guarantees the derived
hash is byte-identical to the previously stamped one, so upgrading the
operator rolls zero pods.

### Implementation

Simply removing a field that has had its function duplicated elsewhere.

- The ValkeyNode pod-template builders now derive the config-hash
annotation themselves (nodeServerConfigRollHash), gated on
`spec.serverConfigMapName`
- The render core in config.go is parent-agnostic so cluster and node
sides produce the same output bytes
- The cluster controller no longer computes or threads a config hash,
shrinking the parent→ValkeyNode contract: set the real inputs, call
setDesiredWorkloadRevision.
- `spec.workloadRevision` remains the single roll-control field

### Limitations

During a mixed state, its possible that the operator would cause some
rolls and failovers, however once CRDs and operator is synced up then
this is not expected.

### Testing

Beyond unit tests, tested on a kind cluster to verify that:

- change in maxmemory-policy is applied live and does not cause a pod
roll
- change in io-threads causes a pod roll

### Checklist

Before submitting the PR make sure the following are checked:

- [x] This Pull Request is related to one issue.
- [x] Commit message explains what changed and why
- [x] Tests are added or updated.
- [x] Documentation files are updated.
- [ ] I have run pre-commit locally (`pre-commit run --all-files` or
hooks on commit)

Signed-off-by: Joseph Heyburn <jdheyburn@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Pod template changes restart every cluster node at once

4 participants