-
Notifications
You must be signed in to change notification settings - Fork 74
fix: add node affinity for node where PVC is mounted to workspace's backup Jobs #1677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| package storage | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
||
|
|
@@ -27,6 +28,7 @@ import ( | |
| k8sErrors "k8s.io/apimachinery/pkg/api/errors" | ||
| "k8s.io/apimachinery/pkg/api/resource" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/labels" | ||
| "k8s.io/apimachinery/pkg/types" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
|
||
|
|
@@ -266,6 +268,60 @@ func getSharedPVCWorkspaceCount(namespace string, api sync.ClusterAPI) (total in | |
| return total, nil | ||
| } | ||
|
|
||
| // FindNodeForPVC lists DevWorkspace pods in the given namespace and returns the | ||
| // node name where a running pod mounts the specified PVC. Returns an empty | ||
| // string (and nil error) when no such pod is found. | ||
| 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 { | ||
|
Comment on lines
+274
to
+283
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
fiRepository: 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 -nRepository: devfile/devworkspace-operator Length of output: 8337 Use
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 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| return "", err | ||
| } | ||
| return getNodeNameWithPVC(podList, pvcName), nil | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // NodeAffinityForHostname returns an Affinity that pins a pod to the given node | ||
| // using the kubernetes.io/hostname label. Used by both cleanup and backup Jobs | ||
| // to avoid Multi-Attach errors with ReadWriteOnce PVCs on multi-node clusters. | ||
| 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}, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| func getNodeNameWithPVC(list *corev1.PodList, pvcName string) string { | ||
| for _, pod := range list.Items { | ||
| if pod.Status.Phase == corev1.PodRunning { | ||
| for _, volume := range pod.Spec.Volumes { | ||
| if volume.PersistentVolumeClaim != nil && volume.PersistentVolumeClaim.ClaimName == pvcName { | ||
| return pod.Spec.NodeName | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func checkPVCTerminating(name, namespace string, api sync.ClusterAPI) (bool, error) { | ||
| if name == "" { | ||
| // Should not happen | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 theNonCachingClientguideline 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: passr.NonCachingClientinstead ofr.Clienttoprovstorage.FindNodeForPVC(the reconciler already has this field per its test setup).pkg/provision/storage/cleanup.go#L122-L127: verify whetherclusterAPIexposes an equivalent non-caching client forFindNodeForPVC, and use it if available.Based on path instructions: "Use the cached
Clientfor normal reads in controller code andNonCachingClientonly when freshness is required" and "UseNonCachingClientwhenever 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
Source: Path instructions
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Length of output: 4360
🏁 Script executed:
Length of output: 2068
@rohanKanojia, confirmed: the cleanup path already usesclusterAPI.Client, andclusterAPI.NonCachingClientis available.That pre-existing cached lookup does not remove the race, though:
FindNodeForPVClists 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:r.NonCachingClientclusterAPI.NonCachingClient🧬 Code Graph Analysis Results
controllers/workspace/devworkspace_controller.go(line 110-269)Summary (from this snippet): Initializes a
sync.ClusterAPIvalue with a cachedClient(r.Client) plus aNonCachingClient(r.NonCachingClient), and storesScheme,Logger, andCtx. 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)Summary: Chooses PVC name/path based on the workspace’s storage provisioner; accepts
k8sClient client.Clientplusctxandlog, and returns(pvcName, workspacePath, err).pkg/provision/storage/shared.go(line 274-287)Summary: Lists Pods using
k8sClient.List(...)filtered byconstants.DevWorkspaceIDLabeland returns the node name for the givenpvcName, or an error.pkg/provision/storage/shared.go(line 292-310)Summary: Builds a
NodeAffinitytargetingcorev1.LabelHostname=nodeName. Returns*corev1.Affinity.