admission_test.go 16 KB

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