Merge pull request #9532 from Lyndon-Li/issue-fix-9343
Run the E2E test on kind / get-go-version (push) Failing after 1m33s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 18s
Main CI / Build (push) Failing after 41s

Issue 9343: include PV topology to data mover pod affinities
This commit is contained in:
lyndon-li
2026-02-13 13:14:34 +08:00
committed by GitHub
11 changed files with 542 additions and 67 deletions
+17 -22
View File
@@ -230,14 +230,9 @@ func CollectPodLogs(ctx context.Context, podGetter corev1client.CoreV1Interface,
return nil
}
func ToSystemAffinity(loadAffinities []*LoadAffinity) *corev1api.Affinity {
if len(loadAffinities) == 0 {
return nil
}
nodeSelectorTermList := make([]corev1api.NodeSelectorTerm, 0)
for _, loadAffinity := range loadAffinities {
requirements := []corev1api.NodeSelectorRequirement{}
func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopology *corev1api.NodeSelector) *corev1api.Affinity {
requirements := []corev1api.NodeSelectorRequirement{}
if loadAffinity != nil {
for k, v := range loadAffinity.NodeSelector.MatchLabels {
requirements = append(requirements, corev1api.NodeSelectorRequirement{
Key: k,
@@ -253,25 +248,25 @@ func ToSystemAffinity(loadAffinities []*LoadAffinity) *corev1api.Affinity {
Operator: corev1api.NodeSelectorOperator(exp.Operator),
})
}
nodeSelectorTermList = append(
nodeSelectorTermList,
corev1api.NodeSelectorTerm{
MatchExpressions: requirements,
},
)
}
if len(nodeSelectorTermList) > 0 {
result := new(corev1api.Affinity)
result.NodeAffinity = new(corev1api.NodeAffinity)
result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = new(corev1api.NodeSelector)
result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = nodeSelectorTermList
result := new(corev1api.Affinity)
result.NodeAffinity = new(corev1api.NodeAffinity)
result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = new(corev1api.NodeSelector)
return result
if volumeTopology != nil {
result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = append(result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, volumeTopology.NodeSelectorTerms...)
} else if len(requirements) > 0 {
result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = make([]corev1api.NodeSelectorTerm, 1)
} else {
return nil
}
return nil
for i := range result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[i].MatchExpressions = append(result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[i].MatchExpressions, requirements...)
}
return result
}
func DiagnosePod(pod *corev1api.Pod, events *corev1api.EventList) string {
+255 -39
View File
@@ -747,24 +747,23 @@ func TestCollectPodLogs(t *testing.T) {
func TestToSystemAffinity(t *testing.T) {
tests := []struct {
name string
loadAffinities []*LoadAffinity
loadAffinity *LoadAffinity
volumeTopology *corev1api.NodeSelector
expected *corev1api.Affinity
}{
{
name: "loadAffinity is nil",
},
{
name: "loadAffinity is empty",
loadAffinities: []*LoadAffinity{},
name: "loadAffinity is empty",
loadAffinity: &LoadAffinity{},
},
{
name: "with match label",
loadAffinities: []*LoadAffinity{
{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-1": "value-1",
},
loadAffinity: &LoadAffinity{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-1": "value-1",
},
},
},
@@ -788,23 +787,21 @@ func TestToSystemAffinity(t *testing.T) {
},
{
name: "with match expression",
loadAffinities: []*LoadAffinity{
{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-2": "value-2",
loadAffinity: &LoadAffinity{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-2": "value-2",
},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key-3",
Values: []string{"value-3-1", "value-3-2"},
Operator: metav1.LabelSelectorOpNotIn,
},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key-3",
Values: []string{"value-3-1", "value-3-2"},
Operator: metav1.LabelSelectorOpNotIn,
},
{
Key: "key-4",
Values: []string{"value-4-1", "value-4-2", "value-4-3"},
Operator: metav1.LabelSelectorOpDoesNotExist,
},
{
Key: "key-4",
Values: []string{"value-4-1", "value-4-2", "value-4-3"},
Operator: metav1.LabelSelectorOpDoesNotExist,
},
},
},
@@ -838,19 +835,49 @@ func TestToSystemAffinity(t *testing.T) {
},
},
{
name: "multiple load affinities",
loadAffinities: []*LoadAffinity{
{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-1": "value-1",
name: "with olume topology",
volumeTopology: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-5",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-6",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
},
{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-2": "value-2",
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-7",
Values: []string{"value-7-1", "value-7-2", "value-7-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-8",
Values: []string{"value-8-1", "value-8-2", "value-8-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
{
MatchFields: []corev1api.NodeSelectorRequirement{
{
Key: "key-9",
Values: []string{"value-9-1", "value-9-2", "value-9-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-a",
Values: []string{"value-a-1", "value-a-2", "value-a-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
},
@@ -862,10 +889,177 @@ func TestToSystemAffinity(t *testing.T) {
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-1",
Values: []string{"value-1"},
Key: "key-5",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-6",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-7",
Values: []string{"value-7-1", "value-7-2", "value-7-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-8",
Values: []string{"value-8-1", "value-8-2", "value-8-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
{
MatchFields: []corev1api.NodeSelectorRequirement{
{
Key: "key-9",
Values: []string{"value-9-1", "value-9-2", "value-9-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-a",
Values: []string{"value-a-1", "value-a-2", "value-a-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
},
},
},
},
},
{
name: "with match expression and volume topology",
loadAffinity: &LoadAffinity{
NodeSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"key-2": "value-2",
},
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "key-3",
Values: []string{"value-3-1", "value-3-2"},
Operator: metav1.LabelSelectorOpNotIn,
},
{
Key: "key-4",
Values: []string{"value-4-1", "value-4-2", "value-4-3"},
Operator: metav1.LabelSelectorOpDoesNotExist,
},
},
},
},
volumeTopology: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-5",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-6",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-7",
Values: []string{"value-7-1", "value-7-2", "value-7-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-8",
Values: []string{"value-8-1", "value-8-2", "value-8-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
{
MatchFields: []corev1api.NodeSelectorRequirement{
{
Key: "key-9",
Values: []string{"value-9-1", "value-9-2", "value-9-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-a",
Values: []string{"value-a-1", "value-a-2", "value-a-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
},
},
expected: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-5",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-6",
Values: []string{"value-5-1", "value-5-2", "value-5-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-2",
Values: []string{"value-2"},
Operator: corev1api.NodeSelectorOpIn,
},
{
Key: "key-3",
Values: []string{"value-3-1", "value-3-2"},
Operator: corev1api.NodeSelectorOpNotIn,
},
{
Key: "key-4",
Values: []string{"value-4-1", "value-4-2", "value-4-3"},
Operator: corev1api.NodeSelectorOpDoesNotExist,
},
},
},
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "key-7",
Values: []string{"value-7-1", "value-7-2", "value-7-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-8",
Values: []string{"value-8-1", "value-8-2", "value-8-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-2",
Values: []string{"value-2"},
Operator: corev1api.NodeSelectorOpIn,
},
{
Key: "key-3",
Values: []string{"value-3-1", "value-3-2"},
Operator: corev1api.NodeSelectorOpNotIn,
},
{
Key: "key-4",
Values: []string{"value-4-1", "value-4-2", "value-4-3"},
Operator: corev1api.NodeSelectorOpDoesNotExist,
},
},
},
{
@@ -875,6 +1069,28 @@ func TestToSystemAffinity(t *testing.T) {
Values: []string{"value-2"},
Operator: corev1api.NodeSelectorOpIn,
},
{
Key: "key-3",
Values: []string{"value-3-1", "value-3-2"},
Operator: corev1api.NodeSelectorOpNotIn,
},
{
Key: "key-4",
Values: []string{"value-4-1", "value-4-2", "value-4-3"},
Operator: corev1api.NodeSelectorOpDoesNotExist,
},
},
MatchFields: []corev1api.NodeSelectorRequirement{
{
Key: "key-9",
Values: []string{"value-9-1", "value-9-2", "value-9-3"},
Operator: corev1api.NodeSelectorOpGt,
},
{
Key: "key-a",
Values: []string{"value-a-1", "value-a-2", "value-a-3"},
Operator: corev1api.NodeSelectorOpGt,
},
},
},
},
@@ -886,7 +1102,7 @@ func TestToSystemAffinity(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
affinity := ToSystemAffinity(test.loadAffinities)
affinity := ToSystemAffinity(test.loadAffinity, test.volumeTopology)
assert.True(t, reflect.DeepEqual(affinity, test.expected))
})
}
+26
View File
@@ -580,3 +580,29 @@ func GetPVAttachedNodes(ctx context.Context, pv string, storageClient storagev1.
return nodes, nil
}
func GetVolumeTopology(ctx context.Context, volumeClient corev1client.CoreV1Interface, storageClient storagev1.StorageV1Interface, pvName string, scName string) (*corev1api.NodeSelector, error) {
if pvName == "" || scName == "" {
return nil, errors.Errorf("invalid parameter, pv %s, sc %s", pvName, scName)
}
sc, err := storageClient.StorageClasses().Get(ctx, scName, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrapf(err, "error getting storage class %s", scName)
}
if sc.VolumeBindingMode == nil || *sc.VolumeBindingMode != storagev1api.VolumeBindingWaitForFirstConsumer {
return nil, nil
}
pv, err := volumeClient.PersistentVolumes().Get(ctx, pvName, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrapf(err, "error getting PV %s", pvName)
}
if pv.Spec.NodeAffinity == nil {
return nil, nil
}
return pv.Spec.NodeAffinity.Required, nil
}
+140
View File
@@ -1909,3 +1909,143 @@ func TestGetPVCAttachingNodeOS(t *testing.T) {
})
}
}
func TestGetVolumeTopology(t *testing.T) {
pvWithoutNodeAffinity := &corev1api.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-pv",
},
}
pvWithNodeAffinity := &corev1api.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-pv",
},
Spec: corev1api.PersistentVolumeSpec{
NodeAffinity: &corev1api.VolumeNodeAffinity{
Required: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "fake-key",
},
},
},
},
},
},
},
}
scObjWithoutVolumeBind := &storagev1api.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-storage-class",
},
}
volumeBindImmediate := storagev1api.VolumeBindingImmediate
scObjWithImeediateBind := &storagev1api.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-storage-class",
},
VolumeBindingMode: &volumeBindImmediate,
}
volumeBindWffc := storagev1api.VolumeBindingWaitForFirstConsumer
scObjWithWffcBind := &storagev1api.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-storage-class",
},
VolumeBindingMode: &volumeBindWffc,
}
tests := []struct {
name string
pvName string
scName string
kubeClientObj []runtime.Object
expectedErr string
expected *corev1api.NodeSelector
}{
{
name: "invalid pvName",
scName: "fake-storage-class",
expectedErr: "invalid parameter, pv , sc fake-storage-class",
},
{
name: "invalid scName",
pvName: "fake-pv",
expectedErr: "invalid parameter, pv fake-pv, sc ",
},
{
name: "no sc",
pvName: "fake-pv",
scName: "fake-storage-class",
expectedErr: "error getting storage class fake-storage-class: storageclasses.storage.k8s.io \"fake-storage-class\" not found",
},
{
name: "sc without binding mode",
pvName: "fake-pv",
scName: "fake-storage-class",
kubeClientObj: []runtime.Object{scObjWithoutVolumeBind},
},
{
name: "sc without immediate binding mode",
pvName: "fake-pv",
scName: "fake-storage-class",
kubeClientObj: []runtime.Object{scObjWithImeediateBind},
},
{
name: "get pv fail",
pvName: "fake-pv",
scName: "fake-storage-class",
kubeClientObj: []runtime.Object{scObjWithWffcBind},
expectedErr: "error getting PV fake-pv: persistentvolumes \"fake-pv\" not found",
},
{
name: "pv with no affinity",
pvName: "fake-pv",
scName: "fake-storage-class",
kubeClientObj: []runtime.Object{
scObjWithWffcBind,
pvWithoutNodeAffinity,
},
},
{
name: "pv with affinity",
pvName: "fake-pv",
scName: "fake-storage-class",
kubeClientObj: []runtime.Object{
scObjWithWffcBind,
pvWithNodeAffinity,
},
expected: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "fake-key",
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
var kubeClient kubernetes.Interface = fakeKubeClient
affinity, err := GetVolumeTopology(t.Context(), kubeClient.CoreV1(), kubeClient.StorageV1(), test.pvName, test.scName)
if test.expectedErr != "" {
assert.EqualError(t, err, test.expectedErr)
} else {
assert.Equal(t, test.expected, affinity)
}
})
}
}