...

Source file src/k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/admission_test.go

Documentation: k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package podtolerationrestriction
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  	corev1 "k8s.io/api/core/v1"
    27  	"k8s.io/apimachinery/pkg/api/resource"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apiserver/pkg/admission"
    30  	genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
    31  	admissiontesting "k8s.io/apiserver/pkg/admission/testing"
    32  	"k8s.io/client-go/informers"
    33  	"k8s.io/client-go/kubernetes"
    34  	"k8s.io/client-go/kubernetes/fake"
    35  	api "k8s.io/kubernetes/pkg/apis/core"
    36  	pluginapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
    37  )
    38  
    39  // TestPodAdmission verifies various scenarios involving pod/namespace tolerations
    40  func TestPodAdmission(t *testing.T) {
    41  
    42  	CPU1000m := resource.MustParse("1000m")
    43  	CPU500m := resource.MustParse("500m")
    44  
    45  	burstablePod := &api.Pod{
    46  		ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
    47  		Spec: api.PodSpec{
    48  			Containers: []api.Container{
    49  				{
    50  					Name: "test",
    51  					Resources: api.ResourceRequirements{
    52  						Limits:   api.ResourceList{api.ResourceCPU: CPU1000m},
    53  						Requests: api.ResourceList{api.ResourceCPU: CPU500m},
    54  					},
    55  				},
    56  			},
    57  		},
    58  	}
    59  
    60  	guaranteedPod := &api.Pod{
    61  		ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
    62  		Spec: api.PodSpec{
    63  			Containers: []api.Container{
    64  				{
    65  					Name: "test",
    66  					Resources: api.ResourceRequirements{
    67  						Limits:   api.ResourceList{api.ResourceCPU: CPU1000m},
    68  						Requests: api.ResourceList{api.ResourceCPU: CPU1000m},
    69  					},
    70  				},
    71  			},
    72  		},
    73  	}
    74  
    75  	bestEffortPod := &api.Pod{
    76  		ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
    77  		Spec: api.PodSpec{
    78  			Containers: []api.Container{
    79  				{
    80  					Name: "test",
    81  				},
    82  			},
    83  		},
    84  	}
    85  
    86  	tests := []struct {
    87  		pod                       *api.Pod
    88  		defaultClusterTolerations []api.Toleration
    89  		namespaceTolerations      []api.Toleration
    90  		whitelist                 []api.Toleration
    91  		clusterWhitelist          []api.Toleration
    92  		podTolerations            []api.Toleration
    93  		mergedTolerations         []api.Toleration
    94  		admit                     bool
    95  		testName                  string
    96  	}{
    97  		{
    98  			pod:                       bestEffortPod,
    99  			defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   100  			namespaceTolerations:      nil,
   101  			podTolerations:            []api.Toleration{},
   102  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   103  			admit:                     true,
   104  			testName:                  "default cluster tolerations with empty pod tolerations and nil namespace tolerations",
   105  		},
   106  		{
   107  			pod:                       bestEffortPod,
   108  			defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   109  			namespaceTolerations:      []api.Toleration{},
   110  			podTolerations:            []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   111  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   112  			admit:                     true,
   113  			testName:                  "default cluster tolerations with pod tolerations specified",
   114  		},
   115  		{
   116  			pod:                       bestEffortPod,
   117  			defaultClusterTolerations: []api.Toleration{},
   118  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   119  			podTolerations:            []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   120  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   121  			admit:                     true,
   122  			testName:                  "namespace tolerations",
   123  		},
   124  		{
   125  			pod:                       bestEffortPod,
   126  			defaultClusterTolerations: []api.Toleration{},
   127  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   128  			podTolerations:            []api.Toleration{},
   129  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   130  			admit:                     true,
   131  			testName:                  "no pod tolerations",
   132  		},
   133  		{
   134  			pod:                       bestEffortPod,
   135  			defaultClusterTolerations: []api.Toleration{},
   136  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule"}},
   137  			podTolerations:            []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule"}},
   138  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule"}, {Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule"}},
   139  			admit:                     true,
   140  			testName:                  "duplicate key pod and namespace tolerations",
   141  		},
   142  		{
   143  			pod:                       bestEffortPod,
   144  			defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue2", Effect: "NoSchedule", TolerationSeconds: nil}},
   145  			namespaceTolerations:      []api.Toleration{},
   146  			podTolerations:            []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
   147  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
   148  			admit:                     true,
   149  			testName:                  "conflicting pod and default cluster tolerations but overridden by empty namespace tolerations",
   150  		},
   151  		{
   152  			pod:                       bestEffortPod,
   153  			defaultClusterTolerations: []api.Toleration{},
   154  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   155  			whitelist:                 []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   156  			podTolerations:            []api.Toleration{},
   157  			mergedTolerations:         []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   158  			admit:                     true,
   159  			testName:                  "merged pod tolerations satisfy whitelist",
   160  		},
   161  		{
   162  			pod:                       bestEffortPod,
   163  			defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   164  			namespaceTolerations:      []api.Toleration{},
   165  			podTolerations:            []api.Toleration{},
   166  			mergedTolerations:         []api.Toleration{},
   167  			admit:                     true,
   168  			testName:                  "Override default cluster toleration by empty namespace level toleration",
   169  		},
   170  		{
   171  			pod:               bestEffortPod,
   172  			whitelist:         []api.Toleration{},
   173  			clusterWhitelist:  []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
   174  			podTolerations:    []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   175  			mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   176  			admit:             true,
   177  			testName:          "pod toleration conflicts with default cluster white list which is overridden by empty namespace whitelist",
   178  		},
   179  		{
   180  			pod:                       bestEffortPod,
   181  			defaultClusterTolerations: []api.Toleration{},
   182  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   183  			whitelist:                 []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
   184  			podTolerations:            []api.Toleration{},
   185  			admit:                     false,
   186  			testName:                  "merged pod tolerations conflict with the whitelist",
   187  		},
   188  		{
   189  			pod:                       burstablePod,
   190  			defaultClusterTolerations: []api.Toleration{},
   191  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   192  			whitelist:                 []api.Toleration{},
   193  			podTolerations:            []api.Toleration{},
   194  			mergedTolerations: []api.Toleration{
   195  				{Key: corev1.TaintNodeMemoryPressure, Operator: api.TolerationOpExists, Effect: api.TaintEffectNoSchedule, TolerationSeconds: nil},
   196  				{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil},
   197  			},
   198  			admit:    true,
   199  			testName: "added memoryPressure/DiskPressure for Burstable pod",
   200  		},
   201  		{
   202  			pod:                       bestEffortPod,
   203  			defaultClusterTolerations: []api.Toleration{},
   204  			namespaceTolerations:      []api.Toleration{},
   205  			whitelist:                 []api.Toleration{},
   206  			podTolerations:            []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}, {Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
   207  			mergedTolerations: []api.Toleration{
   208  				{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil},
   209  				{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil},
   210  			},
   211  			admit:    true,
   212  			testName: "Pod with duplicate key tolerations should not be modified",
   213  		},
   214  		{
   215  			pod:                       guaranteedPod,
   216  			defaultClusterTolerations: []api.Toleration{},
   217  			namespaceTolerations:      []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
   218  			whitelist:                 []api.Toleration{},
   219  			podTolerations:            []api.Toleration{},
   220  			mergedTolerations: []api.Toleration{
   221  				{Key: corev1.TaintNodeMemoryPressure, Operator: api.TolerationOpExists, Effect: api.TaintEffectNoSchedule, TolerationSeconds: nil},
   222  				{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil},
   223  			},
   224  			admit:    true,
   225  			testName: "added memoryPressure/DiskPressure for Guaranteed pod",
   226  		},
   227  	}
   228  	for _, test := range tests {
   229  		t.Run(test.testName, func(t *testing.T) {
   230  			namespace := &corev1.Namespace{
   231  				ObjectMeta: metav1.ObjectMeta{
   232  					Name:        "testNamespace",
   233  					Namespace:   "",
   234  					Annotations: map[string]string{},
   235  				},
   236  			}
   237  
   238  			if test.namespaceTolerations != nil {
   239  				tolerationStr, err := json.Marshal(test.namespaceTolerations)
   240  				if err != nil {
   241  					t.Errorf("error in marshalling namespace tolerations %v", test.namespaceTolerations)
   242  				}
   243  				namespace.Annotations = map[string]string{NSDefaultTolerations: string(tolerationStr)}
   244  			}
   245  
   246  			if test.whitelist != nil {
   247  				tolerationStr, err := json.Marshal(test.whitelist)
   248  				if err != nil {
   249  					t.Errorf("error in marshalling namespace whitelist %v", test.whitelist)
   250  				}
   251  				namespace.Annotations[NSWLTolerations] = string(tolerationStr)
   252  			}
   253  
   254  			mockClient := fake.NewSimpleClientset(namespace)
   255  			handler, informerFactory, err := newHandlerForTest(mockClient)
   256  			if err != nil {
   257  				t.Fatalf("unexpected error initializing handler: %v", err)
   258  			}
   259  			stopCh := make(chan struct{})
   260  			defer close(stopCh)
   261  			informerFactory.Start(stopCh)
   262  
   263  			handler.pluginConfig = &pluginapi.Configuration{Default: test.defaultClusterTolerations, Whitelist: test.clusterWhitelist}
   264  			pod := test.pod
   265  			pod.Spec.Tolerations = test.podTolerations
   266  			err = admissiontesting.WithReinvocationTesting(t, handler).Admit(context.TODO(), admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   267  			if test.admit && err != nil {
   268  				t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
   269  			} else if !test.admit && err == nil {
   270  				t.Errorf("Test: %s, expected an error", test.testName)
   271  			}
   272  
   273  			updatedPodTolerations := pod.Spec.Tolerations
   274  			if test.admit {
   275  				assert.ElementsMatch(t, updatedPodTolerations, test.mergedTolerations)
   276  			}
   277  		})
   278  	}
   279  }
   280  
   281  func TestHandles(t *testing.T) {
   282  	for op, shouldHandle := range map[admission.Operation]bool{
   283  		admission.Create:  true,
   284  		admission.Update:  true,
   285  		admission.Connect: false,
   286  		admission.Delete:  false,
   287  	} {
   288  
   289  		pluginConfig, err := loadConfiguration(nil)
   290  		// must not fail
   291  		if err != nil {
   292  			t.Errorf("%v: error reading default configuration", op)
   293  		}
   294  		ptPlugin := NewPodTolerationsPlugin(pluginConfig)
   295  		if e, a := shouldHandle, ptPlugin.Handles(op); e != a {
   296  			t.Errorf("%v: shouldHandle=%t, handles=%t", op, e, a)
   297  		}
   298  	}
   299  }
   300  
   301  func TestIgnoreUpdatingInitializedPod(t *testing.T) {
   302  	mockClient := &fake.Clientset{}
   303  	handler, informerFactory, err := newHandlerForTest(mockClient)
   304  	if err != nil {
   305  		t.Errorf("unexpected error initializing handler: %v", err)
   306  	}
   307  	handler.SetReadyFunc(func() bool { return true })
   308  
   309  	pod := &api.Pod{
   310  		ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
   311  		Spec:       api.PodSpec{},
   312  	}
   313  	podToleration := api.Toleration{
   314  		Key:               "testKey",
   315  		Operator:          "Equal",
   316  		Value:             "testValue1",
   317  		Effect:            "NoSchedule",
   318  		TolerationSeconds: nil,
   319  	}
   320  	pod.Spec.Tolerations = []api.Toleration{podToleration}
   321  
   322  	// this conflicts with pod's Tolerations
   323  	namespaceToleration := podToleration
   324  	namespaceToleration.Value = "testValue2"
   325  	namespaceTolerations := []api.Toleration{namespaceToleration}
   326  	tolerationsStr, err := json.Marshal(namespaceTolerations)
   327  	if err != nil {
   328  		t.Errorf("error in marshalling namespace tolerations %v", namespaceTolerations)
   329  	}
   330  	namespace := &corev1.Namespace{
   331  		ObjectMeta: metav1.ObjectMeta{
   332  			Name:      "testNamespace",
   333  			Namespace: "",
   334  		},
   335  	}
   336  	namespace.Annotations = map[string]string{NSDefaultTolerations: string(tolerationsStr)}
   337  	err = informerFactory.Core().V1().Namespaces().Informer().GetStore().Update(namespace)
   338  	if err != nil {
   339  		t.Fatal(err)
   340  	}
   341  
   342  	// if the update of initialized pod is not ignored, an error will be returned because the pod's Tolerations conflicts with namespace's Tolerations.
   343  	err = admissiontesting.WithReinvocationTesting(t, handler).Admit(context.TODO(), admission.NewAttributesRecord(pod, pod, api.Kind("Pod").WithVersion("version"), "testNamespace", pod.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.CreateOptions{}, false, nil), nil)
   344  	if err != nil {
   345  		t.Errorf("expected no error, got: %v", err)
   346  	}
   347  }
   348  
   349  // newHandlerForTest returns the admission controller configured for testing.
   350  func newHandlerForTest(c kubernetes.Interface) (*Plugin, informers.SharedInformerFactory, error) {
   351  	f := informers.NewSharedInformerFactory(c, 5*time.Minute)
   352  	pluginConfig, err := loadConfiguration(nil)
   353  	// must not fail
   354  	if err != nil {
   355  		return nil, nil, err
   356  	}
   357  	handler := NewPodTolerationsPlugin(pluginConfig)
   358  	pluginInitializer := genericadmissioninitializer.New(c, nil, f, nil, nil, nil, nil)
   359  	pluginInitializer.Initialize(handler)
   360  	err = admission.ValidateInitialization(handler)
   361  	return handler, f, err
   362  }
   363  

View as plain text