1
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
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
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
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
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
350 func newHandlerForTest(c kubernetes.Interface) (*Plugin, informers.SharedInformerFactory, error) {
351 f := informers.NewSharedInformerFactory(c, 5*time.Minute)
352 pluginConfig, err := loadConfiguration(nil)
353
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