From 757928ed76fa196ef62a3dfc149a4e92f9ced1a4 Mon Sep 17 00:00:00 2001 From: lyndon-li <98304688+Lyndon-Li@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:14:26 +0800 Subject: [PATCH] BACKPORT-CONFLICT Signed-off-by: github-actions[bot] --- pkg/backup/actions/csi/pvc_action.go | 22 ++++ pkg/nodeagent/node_agent.go | 37 +++++++ pkg/nodeagent/node_agent_test.go | 152 +++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index da690626b..41a5a1f9f 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -335,7 +335,29 @@ func (p *pvcBackupItemAction) Execute( return nil, nil, "", nil, err } +<<<<<<< HEAD vs, err := p.getVolumeSnapshotReference(context.TODO(), pvc, backup) +======= + // validate that the node-agent daemonset is ready when snapshot data movement with + // the built-in data mover is requested. Without this, the DataUpload CR will be + // created but never processed (the DataUpload controller runs inside node-agent), + // causing the backup to hang until itemOperationTimeout expires. + if boolptr.IsSetToTrue(backup.Spec.SnapshotMoveData) && datamover.IsBuiltInDataMover(backup.Spec.DataMover) { + if err := nodeagent.IsReady(ctx, backup.Namespace, p.crClient); err != nil { + p.log.WithError(err).Error("cannot perform snapshot data movement without running node-agent pods") + return nil, nil, "", nil, errors.Wrap(err, "CSI PVC BIA cannot proceed: node-agent is not ready for snapshot data movement") + } + } + + policySnapshotClass, scErr := vh.GetSnapshotClass(item, kuberesource.PersistentVolumeClaims) + if scErr != nil { + p.log.WithError(scErr).Warn("failed to get snapshotClass from volume policy, proceeding without it") + } else if policySnapshotClass != "" { + p.log.Infof("Volume policy specifies snapshotClass=%s for PVC %s/%s", policySnapshotClass, pvc.Namespace, pvc.Name) + } + + vs, err := p.getVolumeSnapshotReference(ctx, pvc, backup, policySnapshotClass) +>>>>>>> 41687279a (fix node agent rediness check issue (#10397)) if err != nil { return nil, nil, "", nil, err } diff --git a/pkg/nodeagent/node_agent.go b/pkg/nodeagent/node_agent.go index 61720c99d..b1b1e1a72 100644 --- a/pkg/nodeagent/node_agent.go +++ b/pkg/nodeagent/node_agent.go @@ -22,6 +22,10 @@ import ( "fmt" "github.com/cockroachdb/errors" +<<<<<<< HEAD +======= + appsv1api "k8s.io/api/apps/v1" +>>>>>>> 41687279a (fix node agent rediness check issue (#10397)) corev1api "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -80,6 +84,39 @@ func KbClientIsRunningInNode(ctx context.Context, namespace string, nodeName str return isRunningInNode(ctx, namespace, nodeName, nil, kubeClient) } +<<<<<<< HEAD +======= +// IsReady checks whether the node-agent daemonset has at least one ready pod +// by inspecting the DaemonSet status. +func IsReady(ctx context.Context, namespace string, crClient ctrlclient.Client) error { + dsLinux := new(appsv1api.DaemonSet) + if err := crClient.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: daemonSet}, dsLinux); err != nil { + dsLinux = nil + if !apierrors.IsNotFound(err) { + return errors.Wrap(err, "failed to get linux node-agent daemonset") + } + } + + dsWindows := new(appsv1api.DaemonSet) + if err := crClient.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: daemonsetWindows}, dsWindows); err != nil { + dsWindows = nil + if !apierrors.IsNotFound(err) { + return errors.Wrap(err, "failed to get windows node-agent daemonset") + } + } + + if dsLinux != nil && dsLinux.Status.NumberReady > 0 { + return nil + } + + if dsWindows != nil && dsWindows.Status.NumberReady > 0 { + return nil + } + + return errors.New("node-agent is not ready: no ready pods found") +} + +>>>>>>> 41687279a (fix node agent rediness check issue (#10397)) // IsRunningInNode checks if the node agent pod is running properly in a specified node through controller client. If not, return the error found func IsRunningInNode(ctx context.Context, namespace string, nodeName string, crClient ctrlclient.Client) error { return isRunningInNode(ctx, namespace, nodeName, crClient, nil) diff --git a/pkg/nodeagent/node_agent_test.go b/pkg/nodeagent/node_agent_test.go index 36b154a75..aa05da621 100644 --- a/pkg/nodeagent/node_agent_test.go +++ b/pkg/nodeagent/node_agent_test.go @@ -17,6 +17,7 @@ limitations under the License. package nodeagent import ( + "context" "testing" "github.com/cockroachdb/errors" @@ -28,7 +29,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" clientTesting "k8s.io/client-go/testing" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" clientFake "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" "github.com/vmware-tanzu/velero/pkg/builder" velerotypes "github.com/vmware-tanzu/velero/pkg/types" @@ -213,6 +216,155 @@ func TestIsRunningInNode(t *testing.T) { } } +<<<<<<< HEAD +======= +func TestIsReady(t *testing.T) { + scheme := runtime.NewScheme() + appsv1api.AddToScheme(scheme) + + dsLinuxNotReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 0}, + } + dsLinuxReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 3}, + } + dsWindowsNotReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent-windows"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 0}, + } + dsWindowsReady := &appsv1api.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "fake-ns", Name: "node-agent-windows"}, + Status: appsv1api.DaemonSetStatus{NumberReady: 2}, + } + + tests := []struct { + name string + kubeClientObj []runtime.Object + namespace string + interceptor *interceptor.Funcs + expectErr string + }{ + { + name: "both daemonsets not found", + namespace: "fake-ns", + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "linux daemonset get error", + namespace: "fake-ns", + interceptor: &interceptor.Funcs{ + Get: func(ctx context.Context, c ctrlclient.WithWatch, key ctrlclient.ObjectKey, obj ctrlclient.Object, opts ...ctrlclient.GetOption) error { + if key.Name == "node-agent" { + return errors.New("fake-get-error") + } + return c.Get(ctx, key, obj, opts...) + }, + }, + expectErr: "failed to get linux node-agent daemonset: fake-get-error", + }, + { + name: "windows daemonset get error", + namespace: "fake-ns", + interceptor: &interceptor.Funcs{ + Get: func(ctx context.Context, c ctrlclient.WithWatch, key ctrlclient.ObjectKey, obj ctrlclient.Object, opts ...ctrlclient.GetOption) error { + if key.Name == "node-agent-windows" { + return errors.New("fake-get-error") + } + return c.Get(ctx, key, obj, opts...) + }, + }, + expectErr: "failed to get windows node-agent daemonset: fake-get-error", + }, + { + name: "linux ds exist but no ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsLinuxNotReady, + }, + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "linux ds with ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsLinuxReady, + }, + }, + { + name: "windows ds exist but no ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsWindowsNotReady, + }, + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "windows ds with ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsWindowsReady, + }, + }, + { + name: "both daemonsets exist but no ready pods", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsLinuxNotReady, + dsWindowsNotReady, + }, + expectErr: "node-agent is not ready: no ready pods found", + }, + { + name: "both daemonsets exist, linux ready", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsLinuxReady, + dsWindowsNotReady, + }, + }, + { + name: "both daemonsets exist, windows ready", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsLinuxNotReady, + dsWindowsReady, + }, + }, + { + name: "both daemonsets exist, both ready", + namespace: "fake-ns", + kubeClientObj: []runtime.Object{ + dsLinuxReady, + dsWindowsReady, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + builder := clientFake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(test.kubeClientObj...) + + if test.interceptor != nil { + builder = builder.WithInterceptorFuncs(*test.interceptor) + } + + fakeClient := builder.Build() + + err := IsReady(t.Context(), test.namespace, fakeClient) + if test.expectErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, test.expectErr) + } + }) + } +} + +>>>>>>> 41687279a (fix node agent rediness check issue (#10397)) func TestGetPodSpec(t *testing.T) { podSpec := corev1api.PodSpec{ NodeName: "fake-node",