Skip to content

fix: add node affinity for node where PVC is mounted to workspace's backup Jobs #1677

Open
rohanKanojia wants to merge 1 commit into
devfile:mainfrom
rohankanojia-forks:fix-backup-job-node-affinity
Open

fix: add node affinity for node where PVC is mounted to workspace's backup Jobs #1677
rohanKanojia wants to merge 1 commit into
devfile:mainfrom
rohankanojia-forks:fix-backup-job-node-affinity

Conversation

@rohanKanojia

@rohanKanojia rohanKanojia commented Jul 24, 2026

Copy link
Copy Markdown
Member

What does this PR do?

I noticed this issue while testing pre-release-testing claude workflow where Backup restore test scripts failed on clusterbot clusters.

Note: The Multi-Attach error depends on Kubernetes scheduling the backup Job pod on a different node than where the PVC is currently mounted. This is non-deterministic: if both workspaces and the backup Job happen to land on the same node, the error won't occur even without the fix.

  • On multi-node clusters, backup Jobs for workspaces using per-user (common) storage fail with Multi-Attach error when the Job is scheduled on a different node than the one where the ReadWriteOnce PVC is mounted
  • Pin backup Jobs to the correct node using NodeAffinity with kubernetes.io/hostname, matching the existing pattern used by cleanup Jobs
  • Consolidate duplicated node-lookup logic from cleanup.go and the backup controller into a shared FindNodeForPVC function in pkg/provision/storage/shared.go

What issues does this PR fix or reference?

Fix #1675

By default, claim-devworkspace PVC uses ReadWriteOnce access mode, meaning it can only be attached to one node at a time. When another workspace is running and has the PVC mounted, the backup Job must be scheduled on the same node. Without node affinity, Kubernetes may schedule the Job on a different node, causing:

Warning  FailedAttachVolume  attachdetach-controller  Multi-Attach error for volume "pvc-xxx" Volume is already used by pod(s) workspace-xxx

The cleanup Job in cleanup.go already solved this by looking up which node has the PVC mounted and setting NodeAffinity. This PR:

  1. Extracts the shared logic into FindNodeForPVC(ctx, client, namespace, pvcName) in pkg/provision/storage/shared.go
  2. Refactors cleanup.go to use the shared function (removing ~37 lines of duplicated code)
  3. Adds the same node affinity to backup Jobs in the BackupCronJobReconciler
  4. Applies node affinity unconditionally for both per-user and per-workspace storage (harmless for per-workspace since there's no PVC contention)

Is it tested? How?

Managed to reproduce the Multi-Attach error in a cluster bot cluster while running the backup/restore workflow.

Test case 1: NodeAffinity is added when another workspace is running (2 DevWorkspaces: 1 running, 1 stopped)

This verifies that when a running workspace has the PVC mounted, the backup Job for the stopped workspace gets NodeAffinity pinning it to the same node.

  1. Deploy DWO from the fix-backup-job-node-affinity branch to the cluster

  2. Enable backup in the DevWorkspaceOperatorConfig:

    oc patch devworkspaceoperatorconfig devworkspace-operator-config \
      -n openshift-operators --type=merge \
      -p '{"config":{"workspace":{"backupCronJob":{"enable":true,"schedule":"* * * * *","registry":{"path":"image-registry.openshift-image-registry.svc:5000"},"oras":{"extraArgs":"--insecure"}}}}}'
  3. Create a test namespace:

    oc new-project dwo-backup-test
  4. Create two DevWorkspaces that share the common PVC (claim-devworkspace):

    kubectl apply -f - <<EOF
    apiVersion: workspace.devfile.io/v1alpha2
    kind: DevWorkspace
    metadata:
      name: test-workspace-a
      namespace: dwo-backup-test
    spec:
      started: true
      template:
        projects:
          - name: web-nodejs-sample
            git:
              remotes:
                origin: "https://github.com/che-samples/web-nodejs-sample.git"
        components:
          - name: dev
            container:
              image: quay.io/devfile/universal-developer-image:latest
    EOF
    kubectl apply -f - <<EOF
    apiVersion: workspace.devfile.io/v1alpha2
    kind: DevWorkspace
    metadata:
      name: test-workspace-b
      namespace: dwo-backup-test
    spec:
      started: true
      template:
        projects:
          - name: web-nodejs-sample
            git:
              remotes:
                origin: "https://github.com/che-samples/web-nodejs-sample.git"
        components:
          - name: dev
            container:
              image: quay.io/devfile/universal-developer-image:latest
    EOF

    Both workspaces use the default common storage type, so they share the claim-devworkspace PVC.

  5. Wait for both workspaces to reach Running state:

    oc get dw -n dwo-backup-test -w
  6. Stop one of the workspaces (this triggers a backup Job on the next cron tick):

    oc patch dw test-workspace-a -n dwo-backup-test --type=merge \
      -p '{"spec":{"started":false}}'
  7. Wait for the backup Job to be created (within 1 minute based on cron schedule):

    oc get jobs -n dwo-backup-test -w
  8. Verify the backup Job has NodeAffinity pinning it to the node where test-workspace-b is running:

    oc get jobs -n dwo-backup-test -o yaml | grep -A 10 nodeAffinity

    Expected output:

    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values:
            - <node-where-test-workspace-b-is-running>
  9. The backup Job pod should start successfully without Multi-Attach errors

Test case 2: NodeAffinity is not added when no other workspace is running (1 DevWorkspace, stopped)

This verifies that when no running workspace has the PVC mounted, the backup Job is created without NodeAffinity (since there's no contention risk).

  1. Deploy DWO from the fix-backup-job-node-affinity branch to the cluster

  2. Enable backup in the DevWorkspaceOperatorConfig:

    oc patch devworkspaceoperatorconfig devworkspace-operator-config \
      -n openshift-operators --type=merge \
      -p '{"config":{"workspace":{"backupCronJob":{"enable":true,"schedule":"* * * * *","registry":{"path":"image-registry.openshift-image-registry.svc:5000"},"oras":{"extraArgs":"--insecure"}}}}}'
  3. Create a test namespace:

    oc new-project dwo-backup-test
  4. Create a single DevWorkspace:

    kubectl apply -f - <<EOF
    apiVersion: workspace.devfile.io/v1alpha2
    kind: DevWorkspace
    metadata:
      name: test-workspace-a
      namespace: dwo-backup-test
    spec:
      started: true
      template:
        projects:
          - name: web-nodejs-sample
            git:
              remotes:
                origin: "https://github.com/che-samples/web-nodejs-sample.git"
        components:
          - name: dev
            container:
              image: quay.io/devfile/universal-developer-image:latest
    EOF
  5. Wait for the workspace to reach Running state:

    oc get dw -n dwo-backup-test -w
  6. Stop the workspace:

    oc patch dw test-workspace-a -n dwo-backup-test --type=merge \
      -p '{"spec":{"started":false}}'
  7. Wait for the backup Job to be created (within 1 minute based on cron schedule):

    oc get jobs -n dwo-backup-test -w
  8. Verify the backup Job does not have NodeAffinity (since no running pod mounts the PVC):

    oc get jobs -n dwo-backup-test -o yaml | grep -c nodeAffinity

    Expected output: 0

  9. The backup Job pod should complete successfully (no contention for the PVC)

PR Checklist

  • E2E tests pass (when PR is ready, comment /test v8-devworkspace-operator-e2e, v8-che-happy-path to trigger)
    • v8-devworkspace-operator-e2e: DevWorkspace e2e test
    • v8-che-happy-path: Happy path for verification integration with Che

Summary by CodeRabbit

  • Bug Fixes

    • Backup cron jobs now preferentially schedule on the node currently mounting the workspace’s PVC, improving backup reliability.
    • Storage cleanup jobs now use the same PVC-to-node detection, and automatically leave scheduling constraints unset when no matching running pod exists.
  • Tests

    • Added unit tests to validate node-affinity behavior for both backup execution and PVC cleanup job generation, including matching, non-matching, and multiple-pod scenarios.

@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown

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

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

PVC node scheduling

Layer / File(s) Summary
PVC node lookup and affinity helpers
pkg/provision/storage/shared.go, pkg/provision/storage/shared_test.go
Adds FindNodeForPVC to locate a running labeled Pod mounting a PVC, adds hostname-based affinity construction, and tests matching, filtering, and absent-Pod cases.
Backup Job scheduling
controllers/backupcronjob/..., controllers/backupcronjob/backupcronjob_controller_test.go
Pins backup Jobs to the PVC’s current node when found and leaves affinity unset when no matching running Pod exists.
Cleanup Job scheduling
pkg/provision/storage/cleanup.go, pkg/provision/storage/cleanup_test.go
Replaces local PVC node discovery with the shared lookup and applies the shared affinity helper to cleanup Jobs, with matching test coverage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BackupCronJobController
  participant FindNodeForPVC
  participant KubernetesAPI
  participant BackupJob
  BackupCronJobController->>FindNodeForPVC: Resolve PVC node
  FindNodeForPVC->>KubernetesAPI: List labeled Pods
  KubernetesAPI-->>FindNodeForPVC: Running Pod node
  FindNodeForPVC-->>BackupCronJobController: Target node
  BackupCronJobController->>BackupJob: Set required hostname affinity
Loading

Possibly related PRs

Suggested reviewers: dkwon17, akurinnoy, btjd, ibuziuk

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding node affinity for backup Jobs based on the PVC mount node.
Linked Issues check ✅ Passed The PR addresses #1675 by pinning backup Jobs to the node where the shared PVC is mounted, which matches the reported Multi-Attach failure.
Out of Scope Changes check ✅ Passed The cleanup-job refactor and new helpers/tests support the same node-selection behavior and do not appear unrelated to the issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@tolusha

tolusha commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Hi! I'm che-ai-assistant — I help with your pull requests.

Available commands:

  • /che-ai-assistant generate-che-doc — Generate a documentation PR based on this PR's changes
  • /che-ai-assistant ok-pr-review — Run a comprehensive PR review (summary, code review, deep review, impact analysis)
  • /che-ai-assistant ok-pr-readiness — Ensure PR has validation steps
  • /che-ai-assistant check-pr-test-failures — Analyze failing CI checks, identify root causes, and suggest fixes
  • /che-ai-assistant update-che-e2e-tests — Update Eclipse Che e2e tests
  • /che-ai-assistant claude — Run a free-form instruction on this PR
  • /che-ai-assistant help — Show this help message

@rohanKanojia
rohanKanojia marked this pull request as ready for review July 24, 2026 10:22

@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: 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 `@controllers/backupcronjob/backupcronjob_controller.go`:
- Around line 462-465: The FindNodeForPVC lookups must use fresh, non-cached Pod
scheduling data. In controllers/backupcronjob/backupcronjob_controller.go lines
462-465, update the FindNodeForPVC call to use r.NonCachingClient instead of
r.Client; in pkg/provision/storage/cleanup.go lines 122-127, use clusterAPI’s
equivalent non-caching client if available, otherwise leave the site unchanged
and document that no equivalent exists.
- Around line 466-484: Add an ActiveDeadlineSeconds value to the Job spec when
targetNode is set, alongside the hostname-pinning affinity in the backup Job
construction. Use the existing backup timeout/deadline configuration if
available, ensuring Pods that remain Pending on the pinned node terminate after
the configured limit.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e50314d0-655e-4718-9714-7e09ee17ac34

📥 Commits

Reviewing files that changed from the base of the PR and between 1f86ade and d227890.

📒 Files selected for processing (5)
  • controllers/backupcronjob/backupcronjob_controller.go
  • controllers/backupcronjob/backupcronjob_controller_test.go
  • pkg/provision/storage/cleanup.go
  • pkg/provision/storage/shared.go
  • pkg/provision/storage/shared_test.go

Comment on lines +462 to +465
targetNode, err := provstorage.FindNodeForPVC(ctx, r.Client, workspace.Namespace, pvc.Name)
if err != nil {
log.Error(err, "Failed to find node with PVC, backup Job will not have node affinity", "pvc", pvc.Name)
}

@coderabbitai coderabbitai Bot Jul 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use a non-caching read for FindNodeForPVC's Pod list. Both call sites pass a (likely) cached client to look up live Pod scheduling state, which is exactly the kind of externally-mutated data the NonCachingClient guideline is meant to protect against — a stale cache read here can recreate the Multi-Attach race this PR is fixing.

  • controllers/backupcronjob/backupcronjob_controller.go#L462-L465: pass r.NonCachingClient instead of r.Client to provstorage.FindNodeForPVC (the reconciler already has this field per its test setup).
  • pkg/provision/storage/cleanup.go#L122-L127: verify whether clusterAPI exposes an equivalent non-caching client for FindNodeForPVC, and use it if available.

Based on path instructions: "Use the cached Client for normal reads in controller code and NonCachingClient only when freshness is required" and "Use NonCachingClient whenever fresh, non-cached resource data is required, especially immediately after create/update or when an external system may have changed the object."

📍 Affects 2 files
  • controllers/backupcronjob/backupcronjob_controller.go#L462-L465 (this comment)
  • pkg/provision/storage/cleanup.go#L122-L127
🤖 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 `@controllers/backupcronjob/backupcronjob_controller.go` around lines 462 -
465, The FindNodeForPVC lookups must use fresh, non-cached Pod scheduling data.
In controllers/backupcronjob/backupcronjob_controller.go lines 462-465, update
the FindNodeForPVC call to use r.NonCachingClient instead of r.Client; in
pkg/provision/storage/cleanup.go lines 122-127, use clusterAPI’s equivalent
non-caching client if available, otherwise leave the site unchanged and document
that no equivalent exists.

Source: Path instructions

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The cleanup job (cleanup.go) already uses the cached clusterAPI.Client for the same purpose before this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline pkg/provision/storage/cleanup.go --items all --type function --view expanded
rg -n -C 5 '\bFindNodeForPVC\s*\(' --glob '*.go'
rg -n -C 4 'type\s+ClusterAPI\b|struct\s*\{.*Client|NonCachingClient' --glob '*.go'

Length of output: 4360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cleanup FindNodeForPVC call ---'
sed -n '111,145p' pkg/provision/storage/cleanup.go

printf '%s\n' '--- backup reconciler initialization ---'
rg -n -C 6 'BackupCronJobReconciler\s*\{' --glob '*.go' || true

printf '%s\n' '--- ClusterAPI declaration ---'
rg -n -C 10 'type\s+ClusterAPI\s+struct' --glob '*.go' || true

printf '%s\n' '--- all FindNodeForPVC callers ---'
rg -n -C 3 'FindNodeForPVC\s*\(' --glob '*.go' || true

Length of output: 2068


@rohanKanojia, confirmed: the cleanup path already uses clusterAPI.Client, and clusterAPI.NonCachingClient is available.

That pre-existing cached lookup does not remove the race, though: FindNodeForPVC lists Pods to select mandatory hostname affinity for a Job mounting an RWO PVC. Pod placement and volume attachment can change outside the controller cache, so this decision requires fresh state. The finding remains applicable to both paths:

  • backup: pass r.NonCachingClient
  • cleanup: pass clusterAPI.NonCachingClient

🧬 Code Graph Analysis Results

controllers/workspace/devworkspace_controller.go (line 110-269)

clusterAPI := sync.ClusterAPI{
		Client:           r.Client,
		NonCachingClient: r.NonCachingClient,
		Scheme:           r.Scheme,
		Logger:           reqLogger,
		Ctx:              ctx,
	}

Summary (from this snippet): Initializes a sync.ClusterAPI value with a cached Client (r.Client) plus a NonCachingClient (r.NonCachingClient), and stores Scheme, Logger, and Ctx. No return values; used later by other reconciliation logic to access cluster resources via the chosen client.


pkg/library/storage/storage.go (line 37-69)

func GetWorkspacePVCInfo(
	ctx context.Context,
	workspace *dw.DevWorkspace,
	config *controllerv1alpha1.OperatorConfiguration,
	k8sClient client.Client,
	log logr.Logger,
) (pvcName string, workspacePath string, err error) {
	workspaceWithConfig := &common.DevWorkspaceWithConfig{}
	workspaceWithConfig.DevWorkspace = workspace
	workspaceWithConfig.Config = config

	storageProvisioner, err := storage.GetProvisioner(workspaceWithConfig)
	if err != nil {
		return "", "", err
	}

	if _, ok := storageProvisioner.(*storage.PerWorkspaceStorageProvisioner); ok {
		pvcName := common.PerWorkspacePVCName(workspace.Status.DevWorkspaceId)
		return pvcName, constants.DefaultProjectsSourcesRoot, nil

	} else if _, ok := storageProvisioner.(*storage.CommonStorageProvisioner); ok {
		if !storageProvisioner.NeedsStorage(&workspace.Spec.Template) {
			// No storage provisioned for this workspace
			return "", "", nil
		}
		pvcName := constants.DefaultWorkspacePVCName
		if config.Workspace != nil && config.Workspace.PVCName != "" {
			pvcName = config.Workspace.PVCName
		}
		return pvcName, workspace.Status.DevWorkspaceId + constants.DefaultProjectsSourcesRoot, nil
	}
	return "", "", nil
}

Summary: Chooses PVC name/path based on the workspace’s storage provisioner; accepts k8sClient client.Client plus ctx and log, and returns (pvcName, workspacePath, err).


pkg/provision/storage/shared.go (line 274-287)

func FindNodeForPVC(ctx context.Context, k8sClient client.Client, namespace, pvcName string) (string, error) {
	labelSelector, err := labels.Parse(constants.DevWorkspaceIDLabel)
	if err != nil {
		return "", err
	}
	podList := &corev1.PodList{}
	if err := k8sClient.List(ctx, podList, &client.ListOptions{
		Namespace:     namespace,
		LabelSelector: labelSelector,
	}); err != nil {
		return "", err
	}
	return getNodeNameWithPVC(podList, pvcName), nil
}

Summary: Lists Pods using k8sClient.List(...) filtered by constants.DevWorkspaceIDLabel and returns the node name for the given pvcName, or an error.


pkg/provision/storage/shared.go (line 292-310)

func NodeAffinityForHostname(nodeName string) *corev1.Affinity {
	return &corev1.Affinity{
		NodeAffinity: &corev1.NodeAffinity{
			RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
				NodeSelectorTerms: []corev1.NodeSelectorTerm{
					{
						MatchExpressions: []corev1.NodeSelectorRequirement{
							{
								Key:      corev1.LabelHostname,
								Operator: corev1.NodeSelectorOpIn,
								Values:   []string{nodeName},
							},
						},
					},
				},
			},
		},
	}
}

Summary: Builds a NodeAffinity targeting corev1.LabelHostname=nodeName. Returns *corev1.Affinity.

Comment thread controllers/backupcronjob/backupcronjob_controller.go
…evfile#1675)

On multi-node clusters, backup Jobs for workspaces using ReadWriteOnce PVCs
can fail with Multi-Attach errors when scheduled on a different node than
the one where the PVC is currently mounted. Pin backup Jobs to the correct
node using NodeAffinity, matching the existing pattern in cleanup Jobs.

Consolidate duplicated node-lookup logic from cleanup.go and the backup
controller into a shared FindNodeForPVC function in pkg/provision/storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Rohan Kumar <rohaan@redhat.com>
@rohanKanojia
rohanKanojia force-pushed the fix-backup-job-node-affinity branch from d227890 to 6f2e86a Compare July 24, 2026 12:53

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

🧹 Nitpick comments (1)
controllers/backupcronjob/backupcronjob_controller_test.go (1)

429-499: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add By(...) steps to the new Ginkgo tests.

These It blocks contain several setup, execution, and assertion phases but no By(...) markers, making failures harder to localize and violating the repository test guideline.

As per coding guidelines, Ginkgo tests should use By("...") to document test steps.

Also applies to: 501-538

🤖 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 `@controllers/backupcronjob/backupcronjob_controller_test.go` around lines 429
- 499, Add descriptive By(...) markers throughout the new Ginkgo It blocks,
including the setup of configuration and resources, execution of
executeBackupSync, retrieval of the Job, and affinity assertions. Apply the same
step documentation to the additional test covering lines 501-538, using markers
that clearly identify each phase and preserve the existing test behavior.

Source: Coding guidelines

🤖 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 `@pkg/provision/storage/shared.go`:
- Around line 274-283: The callers of FindNodeForPVC in the backup cronjob
reconciliation and workspace cleanup flows must use their respective
NonCachingClient instead of cached clients. Replace r.Client and
clusterAPI.Client at those call sites while leaving FindNodeForPVC’s lookup
logic unchanged.
- Around line 280-286: Update both FindNodeForPVC callers to distinguish lookup
failures from a valid empty node result: propagate or return non-NotFound errors
so reconciliation retries and does not create a Job without NodeAffinity, while
preserving the existing behavior for NotFound and genuinely empty node lookups.
- Around line 289-310: The NodeAffinityForHostname function currently uses the
Node name directly as the kubernetes.io/hostname label value, which can fail on
cloud nodes where they differ. Resolve the target Node’s kubernetes.io/hostname
label before constructing the affinity, or use a Node-name field selector if
that is the intended contract, and ensure the resulting scheduling constraint
matches the node returned by FindNodeForPVC.

---

Nitpick comments:
In `@controllers/backupcronjob/backupcronjob_controller_test.go`:
- Around line 429-499: Add descriptive By(...) markers throughout the new Ginkgo
It blocks, including the setup of configuration and resources, execution of
executeBackupSync, retrieval of the Job, and affinity assertions. Apply the same
step documentation to the additional test covering lines 501-538, using markers
that clearly identify each phase and preserve the existing test behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 36927a32-e92c-47e0-b7ba-008a9b8c9874

📥 Commits

Reviewing files that changed from the base of the PR and between d227890 and 6f2e86a.

📒 Files selected for processing (6)
  • controllers/backupcronjob/backupcronjob_controller.go
  • controllers/backupcronjob/backupcronjob_controller_test.go
  • pkg/provision/storage/cleanup.go
  • pkg/provision/storage/cleanup_test.go
  • pkg/provision/storage/shared.go
  • pkg/provision/storage/shared_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • controllers/backupcronjob/backupcronjob_controller.go
  • pkg/provision/storage/shared_test.go
  • pkg/provision/storage/cleanup.go

Comment on lines +274 to +283
func FindNodeForPVC(ctx context.Context, k8sClient client.Client, namespace, pvcName string) (string, error) {
labelSelector, err := labels.Parse(constants.DevWorkspaceIDLabel)
if err != nil {
return "", err
}
podList := &corev1.PodList{}
if err := k8sClient.List(ctx, podList, &client.ListOptions{
Namespace: namespace,
LabelSelector: labelSelector,
}); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | grep -E '(^|/)(shared\.go|backupcronjob_controller\.go|cleanup\.go)$' || true

echo
echo "Relevant occurrences:"
rg -n "FindNodeForPVC|NonCachingClient|backupcronjob_controller\.go|cleanup\.go" -S .

echo
echo "Context shared.go around FindNodeForPVC:"
file=$(git ls-files | grep 'pkg/provision/storage/shared.go' | head -n1)
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '240,310p' "$file" | cat -n
fi

echo
echo "Context backup cron job around call:"
file=$(git ls-files | grep 'controllers/backupcronjob/backupcronjob_controller.go' | head -n1)
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '430,485p' "$file" | cat -n
fi

echo
echo "Context cleanup around call:"
file=$(git ls-files | grep 'pkg/provision/storage/cleanup.go' |头 n1 | sed 's/ / /;s/  */ /')
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '90,145p' "$file" | cat -n
fi

Repository: devfile/devworkspace-operator

Length of output: 8245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Backup caller and type fields:"
sed -n '48,70p' controllers/backupcronjob/backupcronjob_controller.go | cat -n
sed -n '445,468p' controllers/backupcronjob/backupcronjob_controller.go | cat -n

echo
echo "Cleanup callers:"
rg -n "FindNodeForPVC\\(" controllers pkg/provision/storage -S
sed -n '110,130p' controllers/workspace/cleanup.go | cat -n
sed -n '90,130p' pkg/provision/storage/cleanup.go | cat -n

echo
echo "ClusterAPI fields:"
rg -n "type ClusterAPI|NonCachingClient|Client.*ClusterAPI|Ctx.*ClusterAPI" -S pkg controllers -g '*.go' | head -n 80
sed -n '1,110p' pkg/provision/sync/cluster_api.go | cat -n

Repository: devfile/devworkspace-operator

Length of output: 8337


Use NonCachingClient for PVC placement lookups.

FindNodeForPVC determines where the PVC is currently mounted, but both callers pass cached clients:

  • controllers/backupcronjob/backupcronjob_controller.go:462 uses r.Client
  • controllers/workspace/cleanup.go:122 uses clusterAPI.Client

Fresh pod data is needed to avoid pinning cleanup or backup Jobs to a stale node and causing ReadWriteOnce PV Multi-Attach failures. Pass each caller’s NonCachingClient.

🤖 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 `@pkg/provision/storage/shared.go` around lines 274 - 283, The callers of
FindNodeForPVC in the backup cronjob reconciliation and workspace cleanup flows
must use their respective NonCachingClient instead of cached clients. Replace
r.Client and clusterAPI.Client at those call sites while leaving
FindNodeForPVC’s lookup logic unchanged.

Source: Coding guidelines

Comment thread pkg/provision/storage/shared.go
Comment thread pkg/provision/storage/shared.go
@tolusha

tolusha commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

/che-ai-assistant ok-pr-readiness

Task completed.

@tolusha

tolusha commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

PR Readiness Assessment: PR #1677 — fix: add node affinity for node where PVC is mounted to workspace's backup Jobs

Repository: devfile/devworkspace-operator
Linked Issue: #1675 — Backup/restore fails for per-user (common) storage on multi-node clusters due to PVC multi-attach


# Criterion Verdict Notes
1 Problem Statement ✅ PASS Clearly describes the Multi-Attach error on multi-node clusters when backup Jobs are scheduled on a different node than where the ReadWriteOnce PVC is mounted. Both PR body and linked issue #1675 provide detailed context including actual error output.
2 Reproduction Steps ✅ PASS Issue #1675 provides a 6-step reproduction path. PR body includes two fully scripted test cases with exact oc/kubectl commands, resource manifests, and prerequisite configuration (multi-node cluster, per-user storage, two DevWorkspaces).
3 Expected Behavior After Fix ✅ PASS Clearly stated: backup Jobs get NodeAffinity pinning them to the node where a running pod mounts the PVC. Test case 1 shows expected YAML output with kubernetes.io/hostname match expression. Test case 2 confirms no affinity is set when no contention exists.
4 Scope of Changes ✅ PASS 6 files changed (+367/−55). All changes are logically related: extracts shared FindNodeForPVC and NodeAffinityForHostname into shared.go, refactors cleanup.go to use them (removing ~37 lines of duplication), adds node affinity to backup controller, and adds comprehensive tests. Rationale for each change is documented.
5 Test Evidence ✅ PASS New unit tests cover both positive and negative cases: backup controller tests (Job with/without node affinity), cleanup test (node affinity when pod mounts PVC), and table-driven tests for FindNodeForPVC (7 scenarios including running/pending pods, non-matching PVC, multiple pods, unlabeled pods). Manual testing confirmed on a clusterbot cluster.
6 Deployment & Verification Notes ✅ PASS Two detailed test cases provide step-by-step verification with exact commands: (1) verify NodeAffinity is added when another workspace runs on the PVC, (2) verify no NodeAffinity when no contention. Includes DWO deployment from the branch, backup config setup, workspace creation manifests, and specific oc get commands to inspect the result.

Overall: ✅ READY


Missing Information

  • E2E test results are not yet available — the PR checklist shows v8-devworkspace-operator-e2e and v8-che-happy-path tests have not been triggered. Consider commenting /test v8-devworkspace-operator-e2e, v8-che-happy-path to run them before merge.

What's Good

  • Excellent problem description with the actual Kubernetes error message and a clear explanation of why ReadWriteOnce PVC access mode causes this on multi-node clusters but not single-node
  • The note about non-deterministic failure (depends on Kubernetes scheduling) is valuable context for reviewers
  • Clean refactoring approach: extracts shared logic (FindNodeForPVC, NodeAffinityForHostname) rather than duplicating node-lookup code across backup and cleanup controllers
  • Two well-structured manual test cases covering both the positive case (affinity added when PVC is contended) and the negative case (no affinity when no contention)
  • Comprehensive table-driven unit tests for FindNodeForPVC covering 7 scenarios including edge cases (pending pods, unlabeled pods, non-matching PVC names, multiple pods)
  • Linked issue Backup/restore fails for per-user (common) storage on multi-node clusters due to PVC multi-attach #1675 provides additional reproduction context and test-result patterns across storage types and registry configurations

@openshift-ci

openshift-ci Bot commented Jul 24, 2026

Copy link
Copy Markdown

@rohanKanojia: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/v14-che-happy-path 6f2e86a link true /test v14-che-happy-path
ci/prow/v14-devworkspace-operator-e2e 6f2e86a link true /test v14-devworkspace-operator-e2e

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.

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.

Backup/restore fails for per-user (common) storage on multi-node clusters due to PVC multi-attach

2 participants