/* 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 equivalence import ( "errors" "reflect" "testing" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/kubernetes/pkg/scheduler/algorithm" "k8s.io/kubernetes/pkg/scheduler/algorithm/predicates" schedulercache "k8s.io/kubernetes/pkg/scheduler/cache" ) // makeBasicPod returns a Pod object with many of the fields populated. func makeBasicPod(name string) *v1.Pod { isController := true return &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "test-ns", Labels: map[string]string{"app": "web", "env": "prod"}, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "ReplicationController", Name: "rc", UID: "123", Controller: &isController, }, }, }, Spec: v1.PodSpec{ Affinity: &v1.Affinity{ NodeAffinity: &v1.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ NodeSelectorTerms: []v1.NodeSelectorTerm{ { MatchExpressions: []v1.NodeSelectorRequirement{ { Key: "failure-domain.beta.kubernetes.io/zone", Operator: "Exists", }, }, }, }, }, }, PodAffinity: &v1.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "db"}}, TopologyKey: "kubernetes.io/hostname", }, }, }, PodAntiAffinity: &v1.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "web"}}, TopologyKey: "kubernetes.io/hostname", }, }, }, }, InitContainers: []v1.Container{ { Name: "init-pause", Image: "gcr.io/google_containers/pause", Resources: v1.ResourceRequirements{ Limits: v1.ResourceList{ "cpu": resource.MustParse("1"), "mem": resource.MustParse("100Mi"), }, }, }, }, Containers: []v1.Container{ { Name: "pause", Image: "gcr.io/google_containers/pause", Resources: v1.ResourceRequirements{ Limits: v1.ResourceList{ "cpu": resource.MustParse("1"), "mem": resource.MustParse("100Mi"), }, }, VolumeMounts: []v1.VolumeMount{ { Name: "nfs", MountPath: "/srv/data", }, }, }, }, NodeSelector: map[string]string{"node-type": "awesome"}, Tolerations: []v1.Toleration{ { Effect: "NoSchedule", Key: "experimental", Operator: "Exists", }, }, Volumes: []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "someEBSVol1", }, }, }, { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "someEBSVol2", }, }, }, { Name: "nfs", VolumeSource: v1.VolumeSource{ NFS: &v1.NFSVolumeSource{ Server: "nfs.corp.example.com", }, }, }, }, }, } } type predicateItemType struct { fit bool reasons []algorithm.PredicateFailureReason } // mockPredicate provides an algorithm.FitPredicate with pre-set return values. type mockPredicate struct { fit bool reasons []algorithm.PredicateFailureReason err error callCount int } func (p *mockPredicate) predicate(*v1.Pod, algorithm.PredicateMetadata, *schedulercache.NodeInfo) (bool, []algorithm.PredicateFailureReason, error) { p.callCount++ return p.fit, p.reasons, p.err } func TestRunPredicate(t *testing.T) { tests := []struct { name string pred mockPredicate expectFit, expectCacheHit, expectCacheWrite bool expectedReasons []algorithm.PredicateFailureReason expectedError string }{ { name: "pod fits/cache hit", pred: mockPredicate{}, expectFit: true, expectCacheHit: true, expectCacheWrite: false, }, { name: "pod fits/cache miss", pred: mockPredicate{fit: true}, expectFit: true, expectCacheHit: false, expectCacheWrite: true, }, { name: "pod doesn't fit/cache miss", pred: mockPredicate{reasons: []algorithm.PredicateFailureReason{predicates.ErrFakePredicate}}, expectFit: false, expectCacheHit: false, expectCacheWrite: true, expectedReasons: []algorithm.PredicateFailureReason{predicates.ErrFakePredicate}, }, { name: "pod doesn't fit/cache hit", pred: mockPredicate{}, expectFit: false, expectCacheHit: true, expectCacheWrite: false, expectedReasons: []algorithm.PredicateFailureReason{predicates.ErrFakePredicate}, }, { name: "predicate error", pred: mockPredicate{err: errors.New("This is expected")}, expectFit: false, expectCacheHit: false, expectCacheWrite: false, expectedError: "This is expected", }, } predicatesOrdering := []string{"testPredicate"} predicateID := 0 for _, test := range tests { t.Run(test.name, func(t *testing.T) { node := schedulercache.NewNodeInfo() testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "n1"}} node.SetNode(testNode) pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "p1"}} meta := algorithm.EmptyPredicateMetadataProducer(nil, nil) // Initialize and populate equivalence class cache. ecache := NewCache(predicatesOrdering) ecache.Snapshot() nodeCache, _ := ecache.GetNodeCache(testNode.Name) equivClass := NewClass(pod) if test.expectCacheHit { nodeCache.updateResult(pod.Name, "testPredicate", predicateID, test.expectFit, test.expectedReasons, equivClass.hash, node) } fit, reasons, err := nodeCache.RunPredicate(test.pred.predicate, "testPredicate", predicateID, pod, meta, node, equivClass) if err != nil { if err.Error() != test.expectedError { t.Errorf("Expected error %v but got %v", test.expectedError, err) } } else if len(test.expectedError) > 0 { t.Errorf("Expected error %v but got nil", test.expectedError) } if fit && !test.expectFit { t.Errorf("pod should not fit") } if !fit && test.expectFit { t.Errorf("pod should fit") } if len(reasons) != len(test.expectedReasons) { t.Errorf("Expected failures: %v but got %v", test.expectedReasons, reasons) } else { for i, reason := range reasons { if reason != test.expectedReasons[i] { t.Errorf("Expected failures: %v but got %v", test.expectedReasons, reasons) break } } } if test.expectCacheHit && test.pred.callCount != 0 { t.Errorf("Predicate should not be called") } if !test.expectCacheHit && test.pred.callCount == 0 { t.Errorf("Predicate should be called") } _, ok := nodeCache.lookupResult(pod.Name, node.Node().Name, "testPredicate", predicateID, equivClass.hash) if !ok && test.expectCacheWrite { t.Errorf("Cache write should happen") } if !test.expectCacheHit && test.expectCacheWrite && !ok { t.Errorf("Cache write should happen") } if !test.expectCacheHit && !test.expectCacheWrite && ok { t.Errorf("Cache write should not happen") } }) } } func TestUpdateResult(t *testing.T) { predicatesOrdering := []string{"GeneralPredicates"} tests := []struct { name string pod string predicateKey string predicateID int nodeName string fit bool reasons []algorithm.PredicateFailureReason equivalenceHash uint64 expectPredicateMap bool expectCacheItem predicateResult }{ { name: "test 1", pod: "testPod", predicateKey: "GeneralPredicates", predicateID: 0, nodeName: "node1", fit: true, equivalenceHash: 123, expectPredicateMap: false, expectCacheItem: predicateResult{ Fit: true, }, }, { name: "test 2", pod: "testPod", predicateKey: "GeneralPredicates", predicateID: 0, nodeName: "node2", fit: false, equivalenceHash: 123, expectPredicateMap: true, expectCacheItem: predicateResult{ Fit: false, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { node := schedulercache.NewNodeInfo() testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}} node.SetNode(testNode) // Initialize and populate equivalence class cache. ecache := NewCache(predicatesOrdering) nodeCache, _ := ecache.GetNodeCache(testNode.Name) if test.expectPredicateMap { predicateItem := predicateResult{ Fit: true, } nodeCache.cache[test.predicateID] = resultMap{ test.equivalenceHash: predicateItem, } } nodeCache.updateResult( test.pod, test.predicateKey, test.predicateID, test.fit, test.reasons, test.equivalenceHash, node, ) cachedMapItem := nodeCache.cache[test.predicateID] if cachedMapItem == nil { t.Errorf("can't find expected cache item: %v", test.expectCacheItem) } else { if !reflect.DeepEqual(cachedMapItem[test.equivalenceHash], test.expectCacheItem) { t.Errorf("expected cached item: %v, but got: %v", test.expectCacheItem, cachedMapItem[test.equivalenceHash]) } } }) } } // slicesEqual wraps reflect.DeepEqual, but returns true when comparing nil and empty slice. func slicesEqual(a, b []algorithm.PredicateFailureReason) bool { if len(a) == 0 && len(b) == 0 { return true } return reflect.DeepEqual(a, b) } func TestLookupResult(t *testing.T) { predicatesOrdering := []string{"GeneralPredicates"} tests := []struct { name string podName string nodeName string predicateKey string predicateID int equivalenceHashForUpdatePredicate uint64 equivalenceHashForCalPredicate uint64 cachedItem predicateItemType expectedPredicateKeyMiss bool expectedEquivalenceHashMiss bool expectedPredicateItem predicateItemType }{ { name: "test 1", podName: "testPod", nodeName: "node1", equivalenceHashForUpdatePredicate: 123, equivalenceHashForCalPredicate: 123, predicateKey: "GeneralPredicates", predicateID: 0, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{predicates.ErrPodNotFitsHostPorts}, }, expectedPredicateKeyMiss: true, expectedPredicateItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{}, }, }, { name: "test 2", podName: "testPod", nodeName: "node2", equivalenceHashForUpdatePredicate: 123, equivalenceHashForCalPredicate: 123, predicateKey: "GeneralPredicates", predicateID: 0, cachedItem: predicateItemType{ fit: true, }, expectedPredicateKeyMiss: false, expectedPredicateItem: predicateItemType{ fit: true, reasons: []algorithm.PredicateFailureReason{}, }, }, { name: "test 3", podName: "testPod", nodeName: "node3", equivalenceHashForUpdatePredicate: 123, equivalenceHashForCalPredicate: 123, predicateKey: "GeneralPredicates", predicateID: 0, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{predicates.ErrPodNotFitsHostPorts}, }, expectedPredicateKeyMiss: false, expectedPredicateItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{predicates.ErrPodNotFitsHostPorts}, }, }, { name: "test 4", podName: "testPod", nodeName: "node4", equivalenceHashForUpdatePredicate: 123, equivalenceHashForCalPredicate: 456, predicateKey: "GeneralPredicates", predicateID: 0, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{predicates.ErrPodNotFitsHostPorts}, }, expectedPredicateKeyMiss: false, expectedEquivalenceHashMiss: true, expectedPredicateItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}} // Initialize and populate equivalence class cache. ecache := NewCache(predicatesOrdering) nodeCache, _ := ecache.GetNodeCache(testNode.Name) node := schedulercache.NewNodeInfo() node.SetNode(testNode) // set cached item to equivalence cache nodeCache.updateResult( test.podName, test.predicateKey, test.predicateID, test.cachedItem.fit, test.cachedItem.reasons, test.equivalenceHashForUpdatePredicate, node, ) // if we want to do invalid, invalid the cached item if test.expectedPredicateKeyMiss { predicateKeys := sets.NewString() predicateKeys.Insert(test.predicateKey) ecache.InvalidatePredicatesOnNode(test.nodeName, predicateKeys) } // calculate predicate with equivalence cache result, ok := nodeCache.lookupResult(test.podName, test.nodeName, test.predicateKey, test.predicateID, test.equivalenceHashForCalPredicate, ) fit, reasons := result.Fit, result.FailReasons // returned invalid should match expectedPredicateKeyMiss or expectedEquivalenceHashMiss if test.equivalenceHashForUpdatePredicate != test.equivalenceHashForCalPredicate { if ok && test.expectedEquivalenceHashMiss { t.Errorf("Failed: %s, expected (equivalence hash) cache miss", test.name) } if !ok && !test.expectedEquivalenceHashMiss { t.Errorf("Failed: %s, expected (equivalence hash) cache hit", test.name) } } else { if ok && test.expectedPredicateKeyMiss { t.Errorf("Failed: %s, expected (predicate key) cache miss", test.name) } if !ok && !test.expectedPredicateKeyMiss { t.Errorf("Failed: %s, expected (predicate key) cache hit", test.name) } } // returned predicate result should match expected predicate item if fit != test.expectedPredicateItem.fit { t.Errorf("Failed: %s, expected fit: %v, but got: %v", test.name, test.cachedItem.fit, fit) } if !slicesEqual(reasons, test.expectedPredicateItem.reasons) { t.Errorf("Failed: %s, expected reasons: %v, but got: %v", test.name, test.expectedPredicateItem.reasons, reasons) } }) } } func TestGetEquivalenceHash(t *testing.T) { pod1 := makeBasicPod("pod1") pod2 := makeBasicPod("pod2") pod3 := makeBasicPod("pod3") pod3.Spec.Volumes = []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "someEBSVol111", }, }, }, } pod4 := makeBasicPod("pod4") pod4.Spec.Volumes = []v1.Volume{ { VolumeSource: v1.VolumeSource{ PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ ClaimName: "someEBSVol222", }, }, }, } pod5 := makeBasicPod("pod5") pod5.Spec.Volumes = []v1.Volume{} pod6 := makeBasicPod("pod6") pod6.Spec.Volumes = nil pod7 := makeBasicPod("pod7") pod7.Spec.NodeSelector = nil pod8 := makeBasicPod("pod8") pod8.Spec.NodeSelector = make(map[string]string) type podInfo struct { pod *v1.Pod hashIsValid bool } tests := []struct { name string podInfoList []podInfo isEquivalent bool }{ { name: "pods with everything the same except name", podInfoList: []podInfo{ {pod: pod1, hashIsValid: true}, {pod: pod2, hashIsValid: true}, }, isEquivalent: true, }, { name: "pods that only differ in their PVC volume sources", podInfoList: []podInfo{ {pod: pod3, hashIsValid: true}, {pod: pod4, hashIsValid: true}, }, isEquivalent: false, }, { name: "pods that have no volumes, but one uses nil and one uses an empty slice", podInfoList: []podInfo{ {pod: pod5, hashIsValid: true}, {pod: pod6, hashIsValid: true}, }, isEquivalent: true, }, { name: "pods that have no NodeSelector, but one uses nil and one uses an empty map", podInfoList: []podInfo{ {pod: pod7, hashIsValid: true}, {pod: pod8, hashIsValid: true}, }, isEquivalent: true, }, } var ( targetPodInfo podInfo targetHash uint64 ) for _, test := range tests { t.Run(test.name, func(t *testing.T) { for i, podInfo := range test.podInfoList { testPod := podInfo.pod eclassInfo := NewClass(testPod) if eclassInfo == nil && podInfo.hashIsValid { t.Errorf("Failed: pod %v is expected to have valid hash", testPod) } if eclassInfo != nil { // NOTE(harry): the first element will be used as target so // this logic can't verify more than two inequivalent pods if i == 0 { targetHash = eclassInfo.hash targetPodInfo = podInfo } else { if targetHash != eclassInfo.hash { if test.isEquivalent { t.Errorf("Failed: pod: %v is expected to be equivalent to: %v", testPod, targetPodInfo.pod) } } } } } }) } } func TestInvalidateCachedPredicateItemOfAllNodes(t *testing.T) { testPredicate := "GeneralPredicates" testPredicateID := 0 predicatesOrdering := []string{testPredicate} // tests is used to initialize all nodes tests := []struct { name string podName string nodeName string equivalenceHashForUpdatePredicate uint64 cachedItem predicateItemType }{ { name: "hash predicate 123 not fits host ports", podName: "testPod", nodeName: "node1", equivalenceHashForUpdatePredicate: 123, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{ predicates.ErrPodNotFitsHostPorts, }, }, }, { name: "hash predicate 456 not fits host ports", podName: "testPod", nodeName: "node2", equivalenceHashForUpdatePredicate: 456, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{ predicates.ErrPodNotFitsHostPorts, }, }, }, { name: "hash predicate 123 fits", podName: "testPod", nodeName: "node3", equivalenceHashForUpdatePredicate: 123, cachedItem: predicateItemType{ fit: true, }, }, } ecache := NewCache(predicatesOrdering) for _, test := range tests { node := schedulercache.NewNodeInfo() testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}} node.SetNode(testNode) nodeCache, _ := ecache.GetNodeCache(testNode.Name) // set cached item to equivalence cache nodeCache.updateResult( test.podName, testPredicate, testPredicateID, test.cachedItem.fit, test.cachedItem.reasons, test.equivalenceHashForUpdatePredicate, node, ) } // invalidate cached predicate for all nodes ecache.InvalidatePredicates(sets.NewString(testPredicate)) // there should be no cached predicate any more for _, test := range tests { t.Run(test.name, func(t *testing.T) { if nodeCache, exist := ecache.nodeToCache[test.nodeName]; exist { if cache := nodeCache.cache[testPredicateID]; cache != nil { t.Errorf("Failed: cached item for predicate key: %v on node: %v should be invalidated", testPredicate, test.nodeName) } } }) } } func TestInvalidateAllCachedPredicateItemOfNode(t *testing.T) { testPredicate := "GeneralPredicates" testPredicateID := 0 predicatesOrdering := []string{testPredicate} // tests is used to initialize all nodes tests := []struct { name string podName string nodeName string equivalenceHashForUpdatePredicate uint64 cachedItem predicateItemType }{ { name: "hash predicate 123 not fits host ports", podName: "testPod", nodeName: "node1", equivalenceHashForUpdatePredicate: 123, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{predicates.ErrPodNotFitsHostPorts}, }, }, { name: "hash predicate 456 not fits host ports", podName: "testPod", nodeName: "node2", equivalenceHashForUpdatePredicate: 456, cachedItem: predicateItemType{ fit: false, reasons: []algorithm.PredicateFailureReason{predicates.ErrPodNotFitsHostPorts}, }, }, { name: "hash predicate 123 fits host ports", podName: "testPod", nodeName: "node3", equivalenceHashForUpdatePredicate: 123, cachedItem: predicateItemType{ fit: true, }, }, } ecache := NewCache(predicatesOrdering) for _, test := range tests { node := schedulercache.NewNodeInfo() testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: test.nodeName}} node.SetNode(testNode) nodeCache, _ := ecache.GetNodeCache(testNode.Name) // set cached item to equivalence cache nodeCache.updateResult( test.podName, testPredicate, testPredicateID, test.cachedItem.fit, test.cachedItem.reasons, test.equivalenceHashForUpdatePredicate, node, ) } for _, test := range tests { t.Run(test.name, func(t *testing.T) { oldNodeCache, _ := ecache.GetNodeCache(test.nodeName) oldGeneration := oldNodeCache.generation // invalidate cached predicate for all nodes ecache.InvalidateAllPredicatesOnNode(test.nodeName) if n, _ := ecache.GetNodeCache(test.nodeName); oldGeneration == n.generation { t.Errorf("Failed: cached item for node: %v should be invalidated", test.nodeName) } }) } } func BenchmarkEquivalenceHash(b *testing.B) { pod := makeBasicPod("test") for i := 0; i < b.N; i++ { getEquivalencePod(pod) } }