mirror of https://github.com/k3s-io/k3s
574 lines
16 KiB
Go
574 lines
16 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package cloud
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"k8s.io/api/core/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
sets "k8s.io/apimachinery/pkg/util/sets"
|
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
core "k8s.io/client-go/testing"
|
|
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
|
|
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
|
|
|
fakecloud "k8s.io/kubernetes/pkg/cloudprovider/providers/fake"
|
|
)
|
|
|
|
func nodeSelectorRequirementsEqual(r1, r2 v1.NodeSelectorRequirement) bool {
|
|
if r1.Key != r2.Key {
|
|
return false
|
|
}
|
|
if r1.Operator != r2.Operator {
|
|
return false
|
|
}
|
|
vals1 := sets.NewString(r1.Values...)
|
|
vals2 := sets.NewString(r2.Values...)
|
|
if vals1.Equal(vals2) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nodeSelectorTermsEqual(t1, t2 v1.NodeSelectorTerm) bool {
|
|
exprs1 := t1.MatchExpressions
|
|
exprs2 := t2.MatchExpressions
|
|
fields1 := t1.MatchFields
|
|
fields2 := t2.MatchFields
|
|
if len(exprs1) != len(exprs2) {
|
|
return false
|
|
}
|
|
if len(fields1) != len(fields2) {
|
|
return false
|
|
}
|
|
match := func(reqs1, reqs2 []v1.NodeSelectorRequirement) bool {
|
|
for _, req1 := range reqs1 {
|
|
reqMatched := false
|
|
for _, req2 := range reqs2 {
|
|
if nodeSelectorRequirementsEqual(req1, req2) {
|
|
reqMatched = true
|
|
break
|
|
}
|
|
}
|
|
if !reqMatched {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return match(exprs1, exprs2) && match(exprs2, exprs1) && match(fields1, fields2) && match(fields2, fields1)
|
|
}
|
|
|
|
// volumeNodeAffinitiesEqual performs a highly semantic comparison of two VolumeNodeAffinity data structures
|
|
// It ignores ordering of instances of NodeSelectorRequirements in a VolumeNodeAffinity's NodeSelectorTerms as well as
|
|
// orderding of strings in Values of NodeSelectorRequirements when matching two VolumeNodeAffinity structures.
|
|
// Note that in most equality functions, Go considers two slices to be not equal if the order of elements in a slice do not
|
|
// match - so reflect.DeepEqual as well as Semantic.DeepEqual do not work for comparing VolumeNodeAffinity semantically.
|
|
// e.g. these two NodeSelectorTerms are considered semantically equal by volumeNodeAffinitiesEqual
|
|
// &VolumeNodeAffinity{Required:&NodeSelector{NodeSelectorTerms:[{[{a In [1]} {b In [2 3]}] []}],},}
|
|
// &VolumeNodeAffinity{Required:&NodeSelector{NodeSelectorTerms:[{[{b In [3 2]} {a In [1]}] []}],},}
|
|
// TODO: move volumeNodeAffinitiesEqual to utils so other can use it too
|
|
func volumeNodeAffinitiesEqual(n1, n2 *v1.VolumeNodeAffinity) bool {
|
|
if (n1 == nil) != (n2 == nil) {
|
|
return false
|
|
}
|
|
if n1 == nil || n2 == nil {
|
|
return true
|
|
}
|
|
ns1 := n1.Required
|
|
ns2 := n2.Required
|
|
|
|
if (ns1 == nil) != (ns2 == nil) {
|
|
return false
|
|
}
|
|
if (ns1 == nil) && (ns2 == nil) {
|
|
return true
|
|
}
|
|
if len(ns1.NodeSelectorTerms) != len(ns1.NodeSelectorTerms) {
|
|
return false
|
|
}
|
|
match := func(terms1, terms2 []v1.NodeSelectorTerm) bool {
|
|
for _, term1 := range terms1 {
|
|
termMatched := false
|
|
for _, term2 := range terms2 {
|
|
if nodeSelectorTermsEqual(term1, term2) {
|
|
termMatched = true
|
|
break
|
|
}
|
|
}
|
|
if !termMatched {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return match(ns1.NodeSelectorTerms, ns2.NodeSelectorTerms) && match(ns2.NodeSelectorTerms, ns1.NodeSelectorTerms)
|
|
}
|
|
|
|
func TestCreatePatch(t *testing.T) {
|
|
ignoredPV := v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "noncloud",
|
|
Initializers: &metav1.Initializers{
|
|
Pending: []metav1.Initializer{
|
|
{
|
|
Name: initializerName,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Spec: v1.PersistentVolumeSpec{
|
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
HostPath: &v1.HostPathVolumeSource{
|
|
Path: "/",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
awsPV := v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "awsPV",
|
|
Initializers: &metav1.Initializers{
|
|
Pending: []metav1.Initializer{
|
|
{
|
|
Name: initializerName,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Spec: v1.PersistentVolumeSpec{
|
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{
|
|
VolumeID: "123",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expectedAffinitya1b2MergedWithAWSPV := v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "a",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1"},
|
|
},
|
|
{
|
|
Key: "b",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expectedAffinityZone1MergedWithAWSPV := v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: kubeletapis.LabelZoneFailureDomain,
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expectedAffinityZonesMergedWithAWSPV := v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: kubeletapis.LabelZoneFailureDomain,
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1", "2", "3"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
awsPVWithAffinity := v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "awsPV",
|
|
Initializers: &metav1.Initializers{
|
|
Pending: []metav1.Initializer{
|
|
{
|
|
Name: initializerName,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Spec: v1.PersistentVolumeSpec{
|
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{
|
|
VolumeID: "123",
|
|
},
|
|
},
|
|
NodeAffinity: &v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "c",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val1", "val2"},
|
|
},
|
|
{
|
|
Key: "d",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val3"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "e",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val4", "val5"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expectedAffinitya1b2MergedWithAWSPVWithAffinity := v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "c",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val1", "val2"},
|
|
},
|
|
{
|
|
Key: "d",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val3"},
|
|
},
|
|
{
|
|
Key: "a",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1"},
|
|
},
|
|
{
|
|
Key: "b",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"2"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "e",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val4", "val5"},
|
|
},
|
|
{
|
|
Key: "a",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1"},
|
|
},
|
|
{
|
|
Key: "b",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expectedAffinityZone1MergedWithAWSPVWithAffinity := v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "c",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val1", "val2"},
|
|
},
|
|
{
|
|
Key: "d",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val3"},
|
|
},
|
|
{
|
|
Key: kubeletapis.LabelZoneFailureDomain,
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "e",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val4", "val5"},
|
|
},
|
|
{
|
|
Key: kubeletapis.LabelZoneFailureDomain,
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
expectedAffinityZonesMergedWithAWSPVWithAffinity := v1.VolumeNodeAffinity{
|
|
Required: &v1.NodeSelector{
|
|
NodeSelectorTerms: []v1.NodeSelectorTerm{
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "c",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val1", "val2"},
|
|
},
|
|
{
|
|
Key: "d",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val3"},
|
|
},
|
|
{
|
|
Key: kubeletapis.LabelZoneFailureDomain,
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"1", "2", "3"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
MatchExpressions: []v1.NodeSelectorRequirement{
|
|
{
|
|
Key: "e",
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"val5", "val4"},
|
|
},
|
|
{
|
|
Key: kubeletapis.LabelZoneFailureDomain,
|
|
Operator: v1.NodeSelectorOpIn,
|
|
Values: []string{"3", "2", "1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
zones, _ := volumeutil.ZonesToSet("1,2,3")
|
|
testCases := map[string]struct {
|
|
vol v1.PersistentVolume
|
|
labels map[string]string
|
|
expectedAffinity *v1.VolumeNodeAffinity
|
|
}{
|
|
"non-cloud PV": {
|
|
vol: ignoredPV,
|
|
labels: nil,
|
|
expectedAffinity: nil,
|
|
},
|
|
"no labels": {
|
|
vol: awsPV,
|
|
labels: nil,
|
|
expectedAffinity: nil,
|
|
},
|
|
"cloudprovider returns nil, nil": {
|
|
vol: awsPV,
|
|
labels: nil,
|
|
expectedAffinity: nil,
|
|
},
|
|
"cloudprovider labels": {
|
|
vol: awsPV,
|
|
labels: map[string]string{"a": "1", "b": "2"},
|
|
expectedAffinity: &expectedAffinitya1b2MergedWithAWSPV,
|
|
},
|
|
"cloudprovider labels pre-existing affinity non-conflicting": {
|
|
vol: awsPVWithAffinity,
|
|
labels: map[string]string{"a": "1", "b": "2"},
|
|
expectedAffinity: &expectedAffinitya1b2MergedWithAWSPVWithAffinity,
|
|
},
|
|
"cloudprovider labels pre-existing affinity conflicting": {
|
|
vol: awsPVWithAffinity,
|
|
labels: map[string]string{"a": "1", "c": "2"},
|
|
expectedAffinity: nil,
|
|
},
|
|
"cloudprovider singlezone": {
|
|
vol: awsPV,
|
|
labels: map[string]string{kubeletapis.LabelZoneFailureDomain: "1"},
|
|
expectedAffinity: &expectedAffinityZone1MergedWithAWSPV,
|
|
},
|
|
"cloudprovider singlezone pre-existing affinity non-conflicting": {
|
|
vol: awsPVWithAffinity,
|
|
labels: map[string]string{kubeletapis.LabelZoneFailureDomain: "1"},
|
|
expectedAffinity: &expectedAffinityZone1MergedWithAWSPVWithAffinity,
|
|
},
|
|
"cloudprovider multizone": {
|
|
vol: awsPV,
|
|
labels: map[string]string{kubeletapis.LabelZoneFailureDomain: volumeutil.ZonesSetToLabelValue(zones)},
|
|
expectedAffinity: &expectedAffinityZonesMergedWithAWSPV,
|
|
},
|
|
"cloudprovider multizone pre-existing affinity non-conflicting": {
|
|
vol: awsPVWithAffinity,
|
|
labels: map[string]string{kubeletapis.LabelZoneFailureDomain: volumeutil.ZonesSetToLabelValue(zones)},
|
|
expectedAffinity: &expectedAffinityZonesMergedWithAWSPVWithAffinity,
|
|
},
|
|
}
|
|
|
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
|
for d, tc := range testCases {
|
|
cloud := &fakecloud.FakeCloud{}
|
|
client := fake.NewSimpleClientset()
|
|
pvlController := NewPersistentVolumeLabelController(client, cloud)
|
|
patch, err := pvlController.createPatch(&tc.vol, tc.labels)
|
|
if err != nil {
|
|
t.Errorf("%s: createPatch returned err: %v", d, err)
|
|
}
|
|
obj := &v1.PersistentVolume{}
|
|
json.Unmarshal(patch, obj)
|
|
if obj.ObjectMeta.Initializers != nil {
|
|
t.Errorf("%s: initializer wasn't removed: %v", d, obj.ObjectMeta.Initializers)
|
|
}
|
|
if tc.labels == nil {
|
|
continue
|
|
}
|
|
for k, v := range tc.labels {
|
|
if obj.ObjectMeta.Labels[k] != v {
|
|
t.Errorf("%s: label %s expected %s got %s", d, k, v, obj.ObjectMeta.Labels[k])
|
|
}
|
|
}
|
|
if !volumeNodeAffinitiesEqual(tc.expectedAffinity, obj.Spec.NodeAffinity) {
|
|
t.Errorf("Expected affinity %v does not match target affinity %v", tc.expectedAffinity, obj.Spec.NodeAffinity)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddLabelsToVolume(t *testing.T) {
|
|
pv := v1.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "awsPV",
|
|
},
|
|
Spec: v1.PersistentVolumeSpec{
|
|
PersistentVolumeSource: v1.PersistentVolumeSource{
|
|
AWSElasticBlockStore: &v1.AWSElasticBlockStoreVolumeSource{
|
|
VolumeID: "123",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
vol v1.PersistentVolume
|
|
initializers *metav1.Initializers
|
|
shouldLabelAndSetAffinity bool
|
|
}{
|
|
"PV without initializer": {
|
|
vol: pv,
|
|
initializers: nil,
|
|
shouldLabelAndSetAffinity: false,
|
|
},
|
|
"PV with initializer to remove": {
|
|
vol: pv,
|
|
initializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: initializerName}}},
|
|
shouldLabelAndSetAffinity: true,
|
|
},
|
|
"PV with other initializers only": {
|
|
vol: pv,
|
|
initializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: "OtherInit"}}},
|
|
shouldLabelAndSetAffinity: false,
|
|
},
|
|
"PV with other initializers first": {
|
|
vol: pv,
|
|
initializers: &metav1.Initializers{Pending: []metav1.Initializer{{Name: "OtherInit"}, {Name: initializerName}}},
|
|
shouldLabelAndSetAffinity: false,
|
|
},
|
|
}
|
|
|
|
utilfeature.DefaultFeatureGate.Set("VolumeScheduling=true")
|
|
defer utilfeature.DefaultFeatureGate.Set("VolumeScheduling=false")
|
|
|
|
for d, tc := range testCases {
|
|
labeledCh := make(chan bool, 1)
|
|
client := fake.NewSimpleClientset()
|
|
client.PrependReactor("patch", "persistentvolumes", func(action core.Action) (handled bool, ret runtime.Object, err error) {
|
|
patch := action.(core.PatchActionImpl).GetPatch()
|
|
obj := &v1.PersistentVolume{}
|
|
json.Unmarshal(patch, obj)
|
|
if obj.ObjectMeta.Labels["a"] != "1" {
|
|
return false, nil, nil
|
|
}
|
|
if obj.Spec.NodeAffinity == nil {
|
|
return false, nil, nil
|
|
}
|
|
if obj.Spec.NodeAffinity.Required == nil {
|
|
return false, nil, nil
|
|
}
|
|
if len(obj.Spec.NodeAffinity.Required.NodeSelectorTerms) == 0 {
|
|
return false, nil, nil
|
|
}
|
|
reqs := obj.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions
|
|
if len(reqs) != 1 {
|
|
return false, nil, nil
|
|
}
|
|
if reqs[0].Key != "a" || reqs[0].Values[0] != "1" || reqs[0].Operator != v1.NodeSelectorOpIn {
|
|
return false, nil, nil
|
|
}
|
|
labeledCh <- true
|
|
return true, nil, nil
|
|
})
|
|
|
|
fakeCloud := &fakecloud.FakeCloud{
|
|
VolumeLabelMap: map[string]map[string]string{"awsPV": {"a": "1"}},
|
|
}
|
|
pvlController := &PersistentVolumeLabelController{kubeClient: client, cloud: fakeCloud}
|
|
tc.vol.ObjectMeta.Initializers = tc.initializers
|
|
pvlController.addLabelsAndAffinityToVolume(&tc.vol)
|
|
|
|
select {
|
|
case l := <-labeledCh:
|
|
if l != tc.shouldLabelAndSetAffinity {
|
|
t.Errorf("%s: label and affinity setting of pv failed. expected %t got %t", d, tc.shouldLabelAndSetAffinity, l)
|
|
}
|
|
case <-time.After(500 * time.Millisecond):
|
|
if tc.shouldLabelAndSetAffinity != false {
|
|
t.Errorf("%s: timed out waiting for label and affinity setting notification", d)
|
|
}
|
|
}
|
|
}
|
|
}
|