admission_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*
  2. Copyright 2019 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 runtimeclass
  14. import (
  15. "context"
  16. "strconv"
  17. "testing"
  18. corev1 "k8s.io/api/core/v1"
  19. v1 "k8s.io/api/core/v1"
  20. "k8s.io/api/node/v1beta1"
  21. "k8s.io/apimachinery/pkg/api/resource"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime"
  24. "k8s.io/apiserver/pkg/admission"
  25. "k8s.io/apiserver/pkg/authentication/user"
  26. utilfeature "k8s.io/apiserver/pkg/util/feature"
  27. featuregatetesting "k8s.io/component-base/featuregate/testing"
  28. "k8s.io/kubernetes/pkg/apis/core"
  29. "k8s.io/kubernetes/pkg/features"
  30. "github.com/stretchr/testify/assert"
  31. )
  32. func newOverheadValidPod(name string, numContainers int, resources core.ResourceRequirements, setOverhead bool) *core.Pod {
  33. pod := &core.Pod{
  34. ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
  35. Spec: core.PodSpec{},
  36. }
  37. pod.Spec.Containers = make([]core.Container, 0, numContainers)
  38. for i := 0; i < numContainers; i++ {
  39. pod.Spec.Containers = append(pod.Spec.Containers, core.Container{
  40. Image: "foo:V" + strconv.Itoa(i),
  41. Resources: resources,
  42. Name: "foo-" + strconv.Itoa(i),
  43. })
  44. }
  45. if setOverhead {
  46. pod.Spec.Overhead = core.ResourceList{
  47. core.ResourceName(core.ResourceCPU): resource.MustParse("100m"),
  48. core.ResourceName(core.ResourceMemory): resource.MustParse("1"),
  49. }
  50. }
  51. return pod
  52. }
  53. func newSchedulingValidPod(name string, nodeSelector map[string]string, tolerations []core.Toleration) *core.Pod {
  54. return &core.Pod{
  55. ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
  56. Spec: core.PodSpec{
  57. NodeSelector: nodeSelector,
  58. Tolerations: tolerations,
  59. },
  60. }
  61. }
  62. func getGuaranteedRequirements() core.ResourceRequirements {
  63. resources := core.ResourceList{
  64. core.ResourceName(core.ResourceCPU): resource.MustParse("1"),
  65. core.ResourceName(core.ResourceMemory): resource.MustParse("10"),
  66. }
  67. return core.ResourceRequirements{Limits: resources, Requests: resources}
  68. }
  69. func TestSetOverhead(t *testing.T) {
  70. tests := []struct {
  71. name string
  72. runtimeClass *v1beta1.RuntimeClass
  73. pod *core.Pod
  74. expectError bool
  75. expectedPod *core.Pod
  76. }{
  77. {
  78. name: "overhead, no container requirements",
  79. runtimeClass: &v1beta1.RuntimeClass{
  80. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  81. Handler: "bar",
  82. Overhead: &v1beta1.Overhead{
  83. PodFixed: corev1.ResourceList{
  84. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
  85. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"),
  86. },
  87. },
  88. },
  89. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, false),
  90. expectError: false,
  91. expectedPod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true),
  92. },
  93. {
  94. name: "overhead, guaranteed pod",
  95. runtimeClass: &v1beta1.RuntimeClass{
  96. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  97. Handler: "bar",
  98. Overhead: &v1beta1.Overhead{
  99. PodFixed: corev1.ResourceList{
  100. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
  101. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"),
  102. },
  103. },
  104. },
  105. pod: newOverheadValidPod("guaranteed", 1, getGuaranteedRequirements(), false),
  106. expectError: false,
  107. expectedPod: newOverheadValidPod("guaranteed", 1, core.ResourceRequirements{}, true),
  108. },
  109. {
  110. name: "overhead, pod with differing overhead already set",
  111. runtimeClass: &v1beta1.RuntimeClass{
  112. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  113. Handler: "bar",
  114. Overhead: &v1beta1.Overhead{
  115. PodFixed: corev1.ResourceList{
  116. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("10"),
  117. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("10G"),
  118. },
  119. },
  120. },
  121. pod: newOverheadValidPod("empty-requiremennts-overhead", 1, core.ResourceRequirements{}, true),
  122. expectError: true,
  123. expectedPod: nil,
  124. },
  125. {
  126. name: "overhead, pod with same overhead already set",
  127. runtimeClass: &v1beta1.RuntimeClass{
  128. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  129. Handler: "bar",
  130. Overhead: &v1beta1.Overhead{
  131. PodFixed: corev1.ResourceList{
  132. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
  133. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"),
  134. },
  135. },
  136. },
  137. pod: newOverheadValidPod("empty-requiremennts-overhead", 1, core.ResourceRequirements{}, true),
  138. expectError: false,
  139. expectedPod: nil,
  140. },
  141. }
  142. for _, tc := range tests {
  143. t.Run(tc.name, func(t *testing.T) {
  144. attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  145. errs := setOverhead(attrs, tc.pod, tc.runtimeClass)
  146. if tc.expectError {
  147. assert.NotEmpty(t, errs)
  148. } else {
  149. assert.Empty(t, errs)
  150. }
  151. })
  152. }
  153. }
  154. func TestSetScheduling(t *testing.T) {
  155. tests := []struct {
  156. name string
  157. runtimeClass *v1beta1.RuntimeClass
  158. pod *core.Pod
  159. expectError bool
  160. expectedPod *core.Pod
  161. }{
  162. {
  163. name: "scheduling, nil scheduling",
  164. runtimeClass: &v1beta1.RuntimeClass{
  165. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  166. Handler: "bar",
  167. Scheduling: nil,
  168. },
  169. pod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, []core.Toleration{}),
  170. expectError: false,
  171. expectedPod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, []core.Toleration{}),
  172. },
  173. {
  174. name: "scheduling, conflict node selector",
  175. runtimeClass: &v1beta1.RuntimeClass{
  176. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  177. Handler: "bar",
  178. Scheduling: &v1beta1.Scheduling{
  179. NodeSelector: map[string]string{
  180. "foo": "conflict",
  181. },
  182. },
  183. },
  184. pod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, []core.Toleration{}),
  185. expectError: true,
  186. },
  187. {
  188. name: "scheduling, nil node selector",
  189. runtimeClass: &v1beta1.RuntimeClass{
  190. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  191. Handler: "bar",
  192. Scheduling: &v1beta1.Scheduling{
  193. NodeSelector: map[string]string{
  194. "foo": "bar",
  195. },
  196. },
  197. },
  198. pod: newSchedulingValidPod("pod-with-conflict-node-selector", nil, nil),
  199. expectError: false,
  200. expectedPod: newSchedulingValidPod("pod-with-conflict-node-selector", map[string]string{"foo": "bar"}, nil),
  201. },
  202. {
  203. name: "scheduling, node selector with the same key value",
  204. runtimeClass: &v1beta1.RuntimeClass{
  205. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  206. Handler: "bar",
  207. Scheduling: &v1beta1.Scheduling{
  208. NodeSelector: map[string]string{
  209. "foo": "bar",
  210. },
  211. },
  212. },
  213. pod: newSchedulingValidPod("pod-with-same-key-value-node-selector", map[string]string{"foo": "bar"}, nil),
  214. expectError: false,
  215. expectedPod: newSchedulingValidPod("pod-with-same-key-value-node-selector", map[string]string{"foo": "bar"}, nil),
  216. },
  217. {
  218. name: "scheduling, node selector with different key value",
  219. runtimeClass: &v1beta1.RuntimeClass{
  220. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  221. Handler: "bar",
  222. Scheduling: &v1beta1.Scheduling{
  223. NodeSelector: map[string]string{
  224. "foo": "bar",
  225. "fizz": "buzz",
  226. },
  227. },
  228. },
  229. pod: newSchedulingValidPod("pod-with-different-key-value-node-selector", map[string]string{"foo": "bar"}, nil),
  230. expectError: false,
  231. expectedPod: newSchedulingValidPod("pod-with-different-key-value-node-selector", map[string]string{"foo": "bar", "fizz": "buzz"}, nil),
  232. },
  233. {
  234. name: "scheduling, multiple tolerations",
  235. runtimeClass: &v1beta1.RuntimeClass{
  236. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  237. Handler: "bar",
  238. Scheduling: &v1beta1.Scheduling{
  239. Tolerations: []v1.Toleration{
  240. {
  241. Key: "foo",
  242. Operator: v1.TolerationOpEqual,
  243. Value: "bar",
  244. Effect: v1.TaintEffectNoSchedule,
  245. },
  246. {
  247. Key: "fizz",
  248. Operator: v1.TolerationOpEqual,
  249. Value: "buzz",
  250. Effect: v1.TaintEffectNoSchedule,
  251. },
  252. },
  253. },
  254. },
  255. pod: newSchedulingValidPod("pod-with-tolerations", map[string]string{"foo": "bar"},
  256. []core.Toleration{
  257. {
  258. Key: "foo",
  259. Operator: core.TolerationOpEqual,
  260. Value: "bar",
  261. Effect: core.TaintEffectNoSchedule,
  262. },
  263. }),
  264. expectError: false,
  265. expectedPod: newSchedulingValidPod("pod-with-tolerations", map[string]string{"foo": "bar"},
  266. []core.Toleration{
  267. {
  268. Key: "foo",
  269. Operator: core.TolerationOpEqual,
  270. Value: "bar",
  271. Effect: core.TaintEffectNoSchedule,
  272. },
  273. {
  274. Key: "fizz",
  275. Operator: core.TolerationOpEqual,
  276. Value: "buzz",
  277. Effect: core.TaintEffectNoSchedule,
  278. },
  279. }),
  280. },
  281. }
  282. for _, tc := range tests {
  283. t.Run(tc.name, func(t *testing.T) {
  284. attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  285. errs := setScheduling(attrs, tc.pod, tc.runtimeClass)
  286. if tc.expectError {
  287. assert.NotEmpty(t, errs)
  288. } else {
  289. assert.Equal(t, tc.expectedPod, tc.pod)
  290. assert.Empty(t, errs)
  291. }
  292. })
  293. }
  294. }
  295. func NewObjectInterfacesForTest() admission.ObjectInterfaces {
  296. scheme := runtime.NewScheme()
  297. corev1.AddToScheme(scheme)
  298. return admission.NewObjectInterfacesFromScheme(scheme)
  299. }
  300. func TestValidate(t *testing.T) {
  301. defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodOverhead, true)()
  302. tests := []struct {
  303. name string
  304. runtimeClass *v1beta1.RuntimeClass
  305. pod *core.Pod
  306. expectError bool
  307. }{
  308. {
  309. name: "No Overhead in RunntimeClass, Overhead set in pod",
  310. runtimeClass: &v1beta1.RuntimeClass{
  311. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  312. Handler: "bar",
  313. },
  314. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, getGuaranteedRequirements(), true),
  315. expectError: true,
  316. },
  317. {
  318. name: "Non-matching Overheads",
  319. runtimeClass: &v1beta1.RuntimeClass{
  320. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  321. Handler: "bar",
  322. Overhead: &v1beta1.Overhead{
  323. PodFixed: corev1.ResourceList{
  324. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("10"),
  325. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("10G"),
  326. },
  327. },
  328. },
  329. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true),
  330. expectError: true,
  331. },
  332. {
  333. name: "Matching Overheads",
  334. runtimeClass: &v1beta1.RuntimeClass{
  335. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  336. Handler: "bar",
  337. Overhead: &v1beta1.Overhead{
  338. PodFixed: corev1.ResourceList{
  339. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
  340. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"),
  341. },
  342. },
  343. },
  344. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, false),
  345. expectError: false,
  346. },
  347. }
  348. rt := NewRuntimeClass()
  349. o := NewObjectInterfacesForTest()
  350. for _, tc := range tests {
  351. t.Run(tc.name, func(t *testing.T) {
  352. attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  353. errs := rt.Validate(context.TODO(), attrs, o)
  354. if tc.expectError {
  355. assert.NotEmpty(t, errs)
  356. } else {
  357. assert.Empty(t, errs)
  358. }
  359. })
  360. }
  361. }
  362. func TestValidateOverhead(t *testing.T) {
  363. tests := []struct {
  364. name string
  365. runtimeClass *v1beta1.RuntimeClass
  366. pod *core.Pod
  367. expectError bool
  368. }{
  369. {
  370. name: "Overhead part of RuntimeClass, no Overhead defined in pod",
  371. runtimeClass: &v1beta1.RuntimeClass{
  372. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  373. Handler: "bar",
  374. Overhead: &v1beta1.Overhead{
  375. PodFixed: corev1.ResourceList{
  376. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
  377. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"),
  378. },
  379. },
  380. },
  381. pod: newOverheadValidPod("no-requirements", 1, core.ResourceRequirements{}, false),
  382. expectError: true,
  383. },
  384. {
  385. name: "No Overhead in RunntimeClass, Overhead set in pod",
  386. runtimeClass: &v1beta1.RuntimeClass{
  387. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  388. Handler: "bar",
  389. },
  390. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, getGuaranteedRequirements(), true),
  391. expectError: true,
  392. },
  393. {
  394. name: "No RunntimeClass, Overhead set in pod",
  395. runtimeClass: nil,
  396. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, getGuaranteedRequirements(), true),
  397. expectError: true,
  398. },
  399. {
  400. name: "Non-matching Overheads",
  401. runtimeClass: &v1beta1.RuntimeClass{
  402. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  403. Handler: "bar",
  404. Overhead: &v1beta1.Overhead{
  405. PodFixed: corev1.ResourceList{
  406. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("10"),
  407. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("10G"),
  408. },
  409. },
  410. },
  411. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true),
  412. expectError: true,
  413. },
  414. {
  415. name: "Matching Overheads",
  416. runtimeClass: &v1beta1.RuntimeClass{
  417. ObjectMeta: metav1.ObjectMeta{Name: "foo"},
  418. Handler: "bar",
  419. Overhead: &v1beta1.Overhead{
  420. PodFixed: corev1.ResourceList{
  421. corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("100m"),
  422. corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1"),
  423. },
  424. },
  425. },
  426. pod: newOverheadValidPod("no-resource-req-no-overhead", 1, core.ResourceRequirements{}, true),
  427. expectError: false,
  428. },
  429. }
  430. for _, tc := range tests {
  431. t.Run(tc.name, func(t *testing.T) {
  432. attrs := admission.NewAttributesRecord(tc.pod, nil, core.Kind("Pod").WithVersion("version"), tc.pod.Namespace, tc.pod.Name, core.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, &user.DefaultInfo{})
  433. errs := validateOverhead(attrs, tc.pod, tc.runtimeClass)
  434. if tc.expectError {
  435. assert.NotEmpty(t, errs)
  436. } else {
  437. assert.Empty(t, errs)
  438. }
  439. })
  440. }
  441. }