From eaf84b6abc14660a72254b7313d045ab1d5a18f8 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 2 Apr 2026 02:06:07 +0530 Subject: [PATCH 1/4] test(requirenodewithfuse): add coverage for Mutate node-selector injection branch Signed-off-by: Harsh --- .../require_node_with_fuse_test.go | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go index 8eaadab9fe5..77caa54bd50 100644 --- a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go +++ b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go @@ -93,5 +93,26 @@ var _ = Describe("RequireNodeWithFuse Plugin", func() { _, err = plugin.Mutate(pod, map[string]base.RuntimeInfoInterface{"pvcName": nil}) Expect(err).To(HaveOccurred()) }) + + It("should inject node selector terms when runtimeInfo has fuse node selectors", func() { + plugin, err := NewPlugin(cl, "") + Expect(err).NotTo(HaveOccurred()) + + runtimeInfo, err := base.BuildRuntimeInfo("test", "fluid", "alluxio") + Expect(err).NotTo(HaveOccurred()) + runtimeInfo.SetFuseNodeSelector(map[string]string{"fluid.io/fuse": "true"}) + + shouldStop, err := plugin.Mutate(pod, map[string]base.RuntimeInfoInterface{"pvcName": runtimeInfo}) + Expect(err).NotTo(HaveOccurred()) + Expect(shouldStop).To(BeFalse()) + Expect(pod.Spec.Affinity).NotTo(BeNil()) + Expect(pod.Spec.Affinity.NodeAffinity).NotTo(BeNil()) + terms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + Expect(terms).To(HaveLen(1)) + Expect(terms[0].MatchExpressions).To(HaveLen(1)) + Expect(terms[0].MatchExpressions[0].Key).To(Equal("fluid.io/fuse")) + Expect(terms[0].MatchExpressions[0].Operator).To(Equal(corev1.NodeSelectorOpIn)) + Expect(terms[0].MatchExpressions[0].Values).To(ConsistOf("true")) + }) }) }) From 532c0f4e5720ebd84e535e23d17dcb045388982b Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 2 Apr 2026 02:12:08 +0530 Subject: [PATCH 2/4] test(requirenodewithfuse): prove multi-term affinity injection appends only into term[0] Signed-off-by: Harsh --- .../require_node_with_fuse_test.go | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go index 77caa54bd50..ab8c7458255 100644 --- a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go +++ b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go @@ -114,5 +114,59 @@ var _ = Describe("RequireNodeWithFuse Plugin", func() { Expect(terms[0].MatchExpressions[0].Operator).To(Equal(corev1.NodeSelectorOpIn)) Expect(terms[0].MatchExpressions[0].Values).To(ConsistOf("true")) }) + + // InjectNodeSelectorTerms appends fuse MatchExpressions only into NodeSelectorTerms[0]. + // A pod with pre-existing terms (A) OR (B) becomes (A AND fuse) OR (B) after injection — + // this is the known upstream semantic: term B can still match a node without fuse. + It("should append fuse match expression into the first existing node selector term when pod already has multiple required node affinity terms", func() { + const fuseKey = "fluid.io/fuse" + const termAKey = "zone" + const termBKey = "region" + + pod.Spec.Affinity = &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + {Key: termAKey, Operator: corev1.NodeSelectorOpIn, Values: []string{"us-east-1a"}}, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + {Key: termBKey, Operator: corev1.NodeSelectorOpIn, Values: []string{"us-east-1"}}, + }, + }, + }, + }, + }, + } + + plugin, err := NewPlugin(cl, "") + Expect(err).NotTo(HaveOccurred()) + + runtimeInfo, err := base.BuildRuntimeInfo("test", "fluid", "alluxio") + Expect(err).NotTo(HaveOccurred()) + runtimeInfo.SetFuseNodeSelector(map[string]string{fuseKey: "true"}) + + shouldStop, err := plugin.Mutate(pod, map[string]base.RuntimeInfoInterface{"pvcName": runtimeInfo}) + Expect(err).NotTo(HaveOccurred()) + Expect(shouldStop).To(BeFalse()) + + terms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + // The two original terms must be preserved; no new term is added. + Expect(terms).To(HaveLen(2)) + // Term[0] gains the fuse expression appended alongside the original zone expression. + Expect(terms[0].MatchExpressions).To(HaveLen(2)) + fuseKeys := make([]string, 0, len(terms[0].MatchExpressions)) + for _, me := range terms[0].MatchExpressions { + fuseKeys = append(fuseKeys, me.Key) + } + Expect(fuseKeys).To(ContainElement(fuseKey)) + Expect(fuseKeys).To(ContainElement(termAKey)) + // Term[1] is left unmodified — it does NOT receive the fuse expression. + Expect(terms[1].MatchExpressions).To(HaveLen(1)) + Expect(terms[1].MatchExpressions[0].Key).To(Equal(termBKey)) + }) }) }) From 1586243c5100c98cf4485f6e1a583ab4704b0140 Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 2 Apr 2026 12:10:35 +0530 Subject: [PATCH 3/4] test(requirenodewithfuse): strengthen multi-term affinity assertions Signed-off-by: Harsh --- .../require_node_with_fuse_test.go | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go index ab8c7458255..2aff178bcf6 100644 --- a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go +++ b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go @@ -158,15 +158,23 @@ var _ = Describe("RequireNodeWithFuse Plugin", func() { Expect(terms).To(HaveLen(2)) // Term[0] gains the fuse expression appended alongside the original zone expression. Expect(terms[0].MatchExpressions).To(HaveLen(2)) - fuseKeys := make([]string, 0, len(terms[0].MatchExpressions)) - for _, me := range terms[0].MatchExpressions { - fuseKeys = append(fuseKeys, me.Key) - } - Expect(fuseKeys).To(ContainElement(fuseKey)) - Expect(fuseKeys).To(ContainElement(termAKey)) + Expect(terms[0].MatchExpressions).To(ContainElement(corev1.NodeSelectorRequirement{ + Key: termAKey, + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1a"}, + })) + Expect(terms[0].MatchExpressions).To(ContainElement(corev1.NodeSelectorRequirement{ + Key: fuseKey, + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + })) // Term[1] is left unmodified — it does NOT receive the fuse expression. Expect(terms[1].MatchExpressions).To(HaveLen(1)) - Expect(terms[1].MatchExpressions[0].Key).To(Equal(termBKey)) + Expect(terms[1].MatchExpressions[0]).To(Equal(corev1.NodeSelectorRequirement{ + Key: termBKey, + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + })) }) }) }) From b7b4a61ef8b470be6854fe5aa260bde91918d6eb Mon Sep 17 00:00:00 2001 From: Harsh Date: Thu, 2 Apr 2026 15:39:05 +0530 Subject: [PATCH 4/4] fix(webhook): preserve required affinity branch semantics Signed-off-by: Harsh --- pkg/utils/webhook.go | 20 +- pkg/utils/webhook_test.go | 365 +++++++++++++++++- .../node_affinity_with_cache_test.go | 107 +++++ .../require_node_with_fuse_test.go | 54 ++- 4 files changed, 522 insertions(+), 24 deletions(-) diff --git a/pkg/utils/webhook.go b/pkg/utils/webhook.go index d06b2caf9c2..c8ad05e922e 100644 --- a/pkg/utils/webhook.go +++ b/pkg/utils/webhook.go @@ -62,10 +62,24 @@ func InjectNodeSelectorTerms(requiredSchedulingTerms []corev1.NodeSelectorTerm, if len(pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms) == 0 { pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = requiredSchedulingTerms } else { - for i := 0; i < len(requiredSchedulingTerms); i++ { - pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions = - append(pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions, requiredSchedulingTerms[i].MatchExpressions...) + existingTerms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + combinedTerms := make([]corev1.NodeSelectorTerm, 0, len(existingTerms)*len(requiredSchedulingTerms)) + for i := 0; i < len(existingTerms); i++ { + if len(existingTerms[i].MatchExpressions) == 0 && len(existingTerms[i].MatchFields) == 0 { + continue + } + for j := 0; j < len(requiredSchedulingTerms); j++ { + if len(requiredSchedulingTerms[j].MatchExpressions) == 0 && len(requiredSchedulingTerms[j].MatchFields) == 0 { + continue + } + combinedTerm := corev1.NodeSelectorTerm{ + MatchExpressions: append(append([]corev1.NodeSelectorRequirement{}, existingTerms[i].MatchExpressions...), requiredSchedulingTerms[j].MatchExpressions...), + MatchFields: append(append([]corev1.NodeSelectorRequirement{}, existingTerms[i].MatchFields...), requiredSchedulingTerms[j].MatchFields...), + } + combinedTerms = append(combinedTerms, combinedTerm) + } } + pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = combinedTerms } } diff --git a/pkg/utils/webhook_test.go b/pkg/utils/webhook_test.go index 6bba1d3099d..3ad92a28437 100644 --- a/pkg/utils/webhook_test.go +++ b/pkg/utils/webhook_test.go @@ -2,6 +2,7 @@ package utils import ( "math/rand" + "reflect" "testing" "time" @@ -73,7 +74,7 @@ func TestInjectNodeSelectorTerms(t *testing.T) { testCases := map[string]struct { nodeSelectorTermList []corev1.NodeSelectorTerm pod *corev1.Pod - expectLen int + expectTerms []corev1.NodeSelectorTerm }{ "test empty nodeSelectorTermList ": { nodeSelectorTermList: []corev1.NodeSelectorTerm{}, @@ -88,7 +89,7 @@ func TestInjectNodeSelectorTerms(t *testing.T) { }, }, }, - expectLen: 0, + expectTerms: []corev1.NodeSelectorTerm{}, }, "test no empty nodeSelectorTermList ": { nodeSelectorTermList: []corev1.NodeSelectorTerm{ @@ -104,7 +105,16 @@ func TestInjectNodeSelectorTerms(t *testing.T) { pod: &corev1.Pod{ Spec: corev1.PodSpec{}, }, - expectLen: 1, + expectTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Operator: corev1.NodeSelectorOpIn, + Values: []string{"test-label-value"}, + }, + }, + }, + }, }, "test add no empty nodeSelectorTermList to pod which alredy have matchExpression": { nodeSelectorTermList: []corev1.NodeSelectorTerm{ @@ -138,21 +148,352 @@ func TestInjectNodeSelectorTerms(t *testing.T) { }, }, }, - expectLen: 2, + expectTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "test", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"test-label-value2"}, + }, + { + Operator: corev1.NodeSelectorOpIn, + Values: []string{"test-label-value"}, + }, + }, + }, + }, + }, + "test cross product with existing and injected or terms": { + nodeSelectorTermList: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "disk", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"ssd"}, + }, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "cpu", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd"}, + }, + }, + }, + }, + pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "zone", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "zone", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + { + Key: "disk", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"ssd"}, + }, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "zone", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + { + Key: "cpu", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd"}, + }, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + { + Key: "disk", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"ssd"}, + }, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + { + Key: "cpu", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"amd"}, + }, + }, + }, + }, + }, + "test skip empty existing term when building cross product": { + nodeSelectorTermList: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "fluid.io/fuse", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + }, + }, + }, + }, + pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + {}, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + }, + { + Key: "fluid.io/fuse", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + }, + }, + }, + }, + }, + "test skip empty injected term when building cross product": { + nodeSelectorTermList: []corev1.NodeSelectorTerm{ + {}, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "fluid.io/fuse", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + }, + }, + }, + }, + pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + }, + { + Key: "fluid.io/fuse", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + }, + }, + }, + }, + }, + "test merge match fields when building cross product": { + nodeSelectorTermList: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "fluid.io/fuse", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + }, + }, + MatchFields: []corev1.NodeSelectorRequirement{ + { + Key: "metadata.name", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"node-b"}, + }, + }, + }, + }, + pod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + }, + }, + MatchFields: []corev1.NodeSelectorRequirement{ + { + Key: "metadata.name", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"node-a"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "region", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"us-east-1"}, + }, + { + Key: "fluid.io/fuse", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + }, + }, + MatchFields: []corev1.NodeSelectorRequirement{ + { + Key: "metadata.name", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"node-a"}, + }, + { + Key: "metadata.name", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"node-b"}, + }, + }, + }, + }, }, } for k, item := range testCases { InjectNodeSelectorTerms(item.nodeSelectorTermList, item.pod) - if k == "test empty nodeSelectorTermList " { - if len(item.pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms) != - item.expectLen { - t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectLen, item.pod.Spec.Affinity.NodeAffinity) + if item.pod.Spec.Affinity == nil || + item.pod.Spec.Affinity.NodeAffinity == nil || + item.pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil { + if len(item.expectTerms) != 0 { + t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:nil", k, item.expectTerms) } - } else { - if len(item.pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions) != - item.expectLen { - t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectLen, item.pod.Spec.Affinity.NodeAffinity) + continue + } + + gotTerms := item.pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + if len(gotTerms) != len(item.expectTerms) { + t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectTerms, gotTerms) + continue + } + + for i := range gotTerms { + if len(gotTerms[i].MatchExpressions) != len(item.expectTerms[i].MatchExpressions) { + t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectTerms, gotTerms) + break + } + + if len(gotTerms[i].MatchFields) != len(item.expectTerms[i].MatchFields) { + t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectTerms, gotTerms) + break + } + + for j := range gotTerms[i].MatchExpressions { + if !reflect.DeepEqual(gotTerms[i].MatchExpressions[j], item.expectTerms[i].MatchExpressions[j]) { + t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectTerms, gotTerms) + break + } + } + + for j := range gotTerms[i].MatchFields { + if !reflect.DeepEqual(gotTerms[i].MatchFields[j], item.expectTerms[i].MatchFields[j]) { + t.Errorf("%s InjectNodeSelectorTerms failure, want:%v, got:%v", k, item.expectTerms, gotTerms) + break + } } } } diff --git a/pkg/webhook/plugins/nodeaffinitywithcache/node_affinity_with_cache_test.go b/pkg/webhook/plugins/nodeaffinitywithcache/node_affinity_with_cache_test.go index 9ea704c411d..73331221f78 100644 --- a/pkg/webhook/plugins/nodeaffinitywithcache/node_affinity_with_cache_test.go +++ b/pkg/webhook/plugins/nodeaffinitywithcache/node_affinity_with_cache_test.go @@ -295,6 +295,113 @@ required: Expect(pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms).To(HaveLen(1)) }) + It("should combine existing required node affinity branches with injected cache locality terms", func() { + plugin, err := NewPlugin(client, customizedTieredLocality) + Expect(err).NotTo(HaveOccurred()) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Labels: map[string]string{ + "fluid.io/dataset." + alluxioRuntime.Name + ".sched": "required", + }, + }, + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "app.kubernetes.io/zone", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"zone-app-a"}, + }, + }, + }, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "kubernetes.io/hostname", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"node-a"}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + _, err = plugin.Mutate(pod, map[string]base.RuntimeInfoInterface{ + alluxioRuntime.Name: runtimeInfo, + }) + Expect(err).NotTo(HaveOccurred()) + + terms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + Expect(terms).To(HaveLen(2)) + + Expect(terms[0].MatchExpressions).To(ContainElements( + corev1.NodeSelectorRequirement{Key: "app.kubernetes.io/zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"zone-app-a"}}, + corev1.NodeSelectorRequirement{Key: runtimeInfo.GetCommonLabelName(), Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}}, + )) + Expect(terms[1].MatchExpressions).To(ContainElements( + corev1.NodeSelectorRequirement{Key: "kubernetes.io/hostname", Operator: corev1.NodeSelectorOpIn, Values: []string{"node-a"}}, + corev1.NodeSelectorRequirement{Key: runtimeInfo.GetCommonLabelName(), Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}}, + )) + }) + + It("should ignore empty existing required node affinity branches when injecting cache locality", func() { + plugin, err := NewPlugin(client, customizedTieredLocality) + Expect(err).NotTo(HaveOccurred()) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Labels: map[string]string{ + "fluid.io/dataset." + alluxioRuntime.Name + ".sched": "required", + }, + }, + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + {}, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "kubernetes.io/hostname", + Operator: corev1.NodeSelectorOpIn, + Values: []string{"node-a"}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + _, err = plugin.Mutate(pod, map[string]base.RuntimeInfoInterface{ + alluxioRuntime.Name: runtimeInfo, + }) + Expect(err).NotTo(HaveOccurred()) + + terms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + Expect(terms).To(HaveLen(1)) + Expect(terms[0].MatchExpressions).To(ContainElements( + corev1.NodeSelectorRequirement{Key: "kubernetes.io/hostname", Operator: corev1.NodeSelectorOpIn, Values: []string{"node-a"}}, + corev1.NodeSelectorRequirement{Key: runtimeInfo.GetCommonLabelName(), Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}}, + )) + }) + It("should mutate pod with tiered locality", func() { plugin, err := NewPlugin(client, customizedTieredLocality) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go index 2aff178bcf6..e02e130cec8 100644 --- a/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go +++ b/pkg/webhook/plugins/requirenodewithfuse/require_node_with_fuse_test.go @@ -115,10 +115,7 @@ var _ = Describe("RequireNodeWithFuse Plugin", func() { Expect(terms[0].MatchExpressions[0].Values).To(ConsistOf("true")) }) - // InjectNodeSelectorTerms appends fuse MatchExpressions only into NodeSelectorTerms[0]. - // A pod with pre-existing terms (A) OR (B) becomes (A AND fuse) OR (B) after injection — - // this is the known upstream semantic: term B can still match a node without fuse. - It("should append fuse match expression into the first existing node selector term when pod already has multiple required node affinity terms", func() { + It("should inject fuse match expression into every existing required node affinity branch", func() { const fuseKey = "fluid.io/fuse" const termAKey = "zone" const termBKey = "region" @@ -154,9 +151,7 @@ var _ = Describe("RequireNodeWithFuse Plugin", func() { Expect(shouldStop).To(BeFalse()) terms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms - // The two original terms must be preserved; no new term is added. Expect(terms).To(HaveLen(2)) - // Term[0] gains the fuse expression appended alongside the original zone expression. Expect(terms[0].MatchExpressions).To(HaveLen(2)) Expect(terms[0].MatchExpressions).To(ContainElement(corev1.NodeSelectorRequirement{ Key: termAKey, @@ -168,13 +163,54 @@ var _ = Describe("RequireNodeWithFuse Plugin", func() { Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}, })) - // Term[1] is left unmodified — it does NOT receive the fuse expression. - Expect(terms[1].MatchExpressions).To(HaveLen(1)) - Expect(terms[1].MatchExpressions[0]).To(Equal(corev1.NodeSelectorRequirement{ + Expect(terms[1].MatchExpressions).To(HaveLen(2)) + Expect(terms[1].MatchExpressions).To(ContainElement(corev1.NodeSelectorRequirement{ Key: termBKey, Operator: corev1.NodeSelectorOpIn, Values: []string{"us-east-1"}, })) + Expect(terms[1].MatchExpressions).To(ContainElement(corev1.NodeSelectorRequirement{ + Key: fuseKey, + Operator: corev1.NodeSelectorOpIn, + Values: []string{"true"}, + })) + }) + + It("should ignore empty existing required node affinity branches when injecting fuse requirements", func() { + const fuseKey = "fluid.io/fuse" + + pod.Spec.Affinity = &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + {}, + { + MatchExpressions: []corev1.NodeSelectorRequirement{ + {Key: "region", Operator: corev1.NodeSelectorOpIn, Values: []string{"us-east-1"}}, + }, + }, + }, + }, + }, + } + + plugin, err := NewPlugin(cl, "") + Expect(err).NotTo(HaveOccurred()) + + runtimeInfo, err := base.BuildRuntimeInfo("test", "fluid", "alluxio") + Expect(err).NotTo(HaveOccurred()) + runtimeInfo.SetFuseNodeSelector(map[string]string{fuseKey: "true"}) + + shouldStop, err := plugin.Mutate(pod, map[string]base.RuntimeInfoInterface{"pvcName": runtimeInfo}) + Expect(err).NotTo(HaveOccurred()) + Expect(shouldStop).To(BeFalse()) + + terms := pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms + Expect(terms).To(HaveLen(1)) + Expect(terms[0].MatchExpressions).To(ContainElements( + corev1.NodeSelectorRequirement{Key: "region", Operator: corev1.NodeSelectorOpIn, Values: []string{"us-east-1"}}, + corev1.NodeSelectorRequirement{Key: fuseKey, Operator: corev1.NodeSelectorOpIn, Values: []string{"true"}}, + )) }) }) })