admission_test.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. /*
  2. Copyright 2015 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 label
  14. import (
  15. "context"
  16. "errors"
  17. "reflect"
  18. "sort"
  19. "testing"
  20. v1 "k8s.io/api/core/v1"
  21. apierrors "k8s.io/apimachinery/pkg/api/errors"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime/schema"
  24. "k8s.io/apiserver/pkg/admission"
  25. admissiontesting "k8s.io/apiserver/pkg/admission/testing"
  26. cloudprovider "k8s.io/cloud-provider"
  27. api "k8s.io/kubernetes/pkg/apis/core"
  28. persistentvolume "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/util"
  29. )
  30. type mockVolumes struct {
  31. volumeLabels map[string]string
  32. volumeLabelsError error
  33. }
  34. var _ cloudprovider.PVLabeler = &mockVolumes{}
  35. func (v *mockVolumes) GetLabelsForVolume(ctx context.Context, pv *v1.PersistentVolume) (map[string]string, error) {
  36. return v.volumeLabels, v.volumeLabelsError
  37. }
  38. func mockVolumeFailure(err error) *mockVolumes {
  39. return &mockVolumes{volumeLabelsError: err}
  40. }
  41. func mockVolumeLabels(labels map[string]string) *mockVolumes {
  42. return &mockVolumes{volumeLabels: labels}
  43. }
  44. func Test_PVLAdmission(t *testing.T) {
  45. testcases := []struct {
  46. name string
  47. handler *persistentVolumeLabel
  48. pvlabeler cloudprovider.PVLabeler
  49. preAdmissionPV *api.PersistentVolume
  50. postAdmissionPV *api.PersistentVolume
  51. err error
  52. }{
  53. {
  54. name: "non-cloud PV ignored",
  55. handler: newPersistentVolumeLabel(),
  56. pvlabeler: mockVolumeLabels(map[string]string{
  57. "a": "1",
  58. "b": "2",
  59. v1.LabelZoneFailureDomain: "1__2__3",
  60. }),
  61. preAdmissionPV: &api.PersistentVolume{
  62. ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
  63. Spec: api.PersistentVolumeSpec{
  64. PersistentVolumeSource: api.PersistentVolumeSource{
  65. HostPath: &api.HostPathVolumeSource{
  66. Path: "/",
  67. },
  68. },
  69. },
  70. },
  71. postAdmissionPV: &api.PersistentVolume{
  72. ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
  73. Spec: api.PersistentVolumeSpec{
  74. PersistentVolumeSource: api.PersistentVolumeSource{
  75. HostPath: &api.HostPathVolumeSource{
  76. Path: "/",
  77. },
  78. },
  79. },
  80. },
  81. err: nil,
  82. },
  83. {
  84. name: "cloud provider error blocks creation of volume",
  85. handler: newPersistentVolumeLabel(),
  86. pvlabeler: mockVolumeFailure(errors.New("invalid volume")),
  87. preAdmissionPV: &api.PersistentVolume{
  88. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  89. Spec: api.PersistentVolumeSpec{
  90. PersistentVolumeSource: api.PersistentVolumeSource{
  91. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  92. VolumeID: "123",
  93. },
  94. },
  95. },
  96. },
  97. postAdmissionPV: &api.PersistentVolume{
  98. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  99. Spec: api.PersistentVolumeSpec{
  100. PersistentVolumeSource: api.PersistentVolumeSource{
  101. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  102. VolumeID: "123",
  103. },
  104. },
  105. },
  106. },
  107. err: apierrors.NewForbidden(schema.ParseGroupResource("persistentvolumes"), "awsebs", errors.New("error querying AWS EBS volume 123: invalid volume")),
  108. },
  109. {
  110. name: "cloud provider returns no labels",
  111. handler: newPersistentVolumeLabel(),
  112. pvlabeler: mockVolumeLabels(map[string]string{}),
  113. preAdmissionPV: &api.PersistentVolume{
  114. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  115. Spec: api.PersistentVolumeSpec{
  116. PersistentVolumeSource: api.PersistentVolumeSource{
  117. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  118. VolumeID: "123",
  119. },
  120. },
  121. },
  122. },
  123. postAdmissionPV: &api.PersistentVolume{
  124. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  125. Spec: api.PersistentVolumeSpec{
  126. PersistentVolumeSource: api.PersistentVolumeSource{
  127. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  128. VolumeID: "123",
  129. },
  130. },
  131. },
  132. },
  133. err: nil,
  134. },
  135. {
  136. name: "cloud provider returns nil, nil",
  137. handler: newPersistentVolumeLabel(),
  138. pvlabeler: mockVolumeFailure(nil),
  139. preAdmissionPV: &api.PersistentVolume{
  140. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  141. Spec: api.PersistentVolumeSpec{
  142. PersistentVolumeSource: api.PersistentVolumeSource{
  143. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  144. VolumeID: "123",
  145. },
  146. },
  147. },
  148. },
  149. postAdmissionPV: &api.PersistentVolume{
  150. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  151. Spec: api.PersistentVolumeSpec{
  152. PersistentVolumeSource: api.PersistentVolumeSource{
  153. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  154. VolumeID: "123",
  155. },
  156. },
  157. },
  158. },
  159. err: nil,
  160. },
  161. {
  162. name: "AWS EBS PV labeled correctly",
  163. handler: newPersistentVolumeLabel(),
  164. pvlabeler: mockVolumeLabels(map[string]string{
  165. "a": "1",
  166. "b": "2",
  167. v1.LabelZoneFailureDomain: "1__2__3",
  168. }),
  169. preAdmissionPV: &api.PersistentVolume{
  170. ObjectMeta: metav1.ObjectMeta{Name: "awsebs", Namespace: "myns"},
  171. Spec: api.PersistentVolumeSpec{
  172. PersistentVolumeSource: api.PersistentVolumeSource{
  173. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  174. VolumeID: "123",
  175. },
  176. },
  177. },
  178. },
  179. postAdmissionPV: &api.PersistentVolume{
  180. ObjectMeta: metav1.ObjectMeta{
  181. Name: "awsebs",
  182. Namespace: "myns",
  183. Labels: map[string]string{
  184. "a": "1",
  185. "b": "2",
  186. v1.LabelZoneFailureDomain: "1__2__3",
  187. },
  188. },
  189. Spec: api.PersistentVolumeSpec{
  190. PersistentVolumeSource: api.PersistentVolumeSource{
  191. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  192. VolumeID: "123",
  193. },
  194. },
  195. NodeAffinity: &api.VolumeNodeAffinity{
  196. Required: &api.NodeSelector{
  197. NodeSelectorTerms: []api.NodeSelectorTerm{
  198. {
  199. MatchExpressions: []api.NodeSelectorRequirement{
  200. {
  201. Key: "a",
  202. Operator: api.NodeSelectorOpIn,
  203. Values: []string{"1"},
  204. },
  205. {
  206. Key: "b",
  207. Operator: api.NodeSelectorOpIn,
  208. Values: []string{"2"},
  209. },
  210. {
  211. Key: v1.LabelZoneFailureDomain,
  212. Operator: api.NodeSelectorOpIn,
  213. Values: []string{"1", "2", "3"},
  214. },
  215. },
  216. },
  217. },
  218. },
  219. },
  220. },
  221. },
  222. err: nil,
  223. },
  224. {
  225. name: "existing labels from dynamic provisioning are not changed",
  226. handler: newPersistentVolumeLabel(),
  227. pvlabeler: mockVolumeLabels(map[string]string{
  228. v1.LabelZoneFailureDomain: "domain1",
  229. v1.LabelZoneRegion: "region1",
  230. }),
  231. preAdmissionPV: &api.PersistentVolume{
  232. ObjectMeta: metav1.ObjectMeta{
  233. Name: "awsebs", Namespace: "myns",
  234. Labels: map[string]string{
  235. v1.LabelZoneFailureDomain: "existingDomain",
  236. v1.LabelZoneRegion: "existingRegion",
  237. },
  238. Annotations: map[string]string{
  239. persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
  240. },
  241. },
  242. Spec: api.PersistentVolumeSpec{
  243. PersistentVolumeSource: api.PersistentVolumeSource{
  244. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  245. VolumeID: "123",
  246. },
  247. },
  248. },
  249. },
  250. postAdmissionPV: &api.PersistentVolume{
  251. ObjectMeta: metav1.ObjectMeta{
  252. Name: "awsebs",
  253. Namespace: "myns",
  254. Labels: map[string]string{
  255. v1.LabelZoneFailureDomain: "existingDomain",
  256. v1.LabelZoneRegion: "existingRegion",
  257. },
  258. Annotations: map[string]string{
  259. persistentvolume.AnnDynamicallyProvisioned: "kubernetes.io/aws-ebs",
  260. },
  261. },
  262. Spec: api.PersistentVolumeSpec{
  263. PersistentVolumeSource: api.PersistentVolumeSource{
  264. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  265. VolumeID: "123",
  266. },
  267. },
  268. NodeAffinity: &api.VolumeNodeAffinity{
  269. Required: &api.NodeSelector{
  270. NodeSelectorTerms: []api.NodeSelectorTerm{
  271. {
  272. MatchExpressions: []api.NodeSelectorRequirement{
  273. {
  274. Key: v1.LabelZoneRegion,
  275. Operator: api.NodeSelectorOpIn,
  276. Values: []string{"existingRegion"},
  277. },
  278. {
  279. Key: v1.LabelZoneFailureDomain,
  280. Operator: api.NodeSelectorOpIn,
  281. Values: []string{"existingDomain"},
  282. },
  283. },
  284. },
  285. },
  286. },
  287. },
  288. },
  289. },
  290. err: nil,
  291. },
  292. {
  293. name: "existing labels from user are changed",
  294. handler: newPersistentVolumeLabel(),
  295. pvlabeler: mockVolumeLabels(map[string]string{
  296. v1.LabelZoneFailureDomain: "domain1",
  297. v1.LabelZoneRegion: "region1",
  298. }),
  299. preAdmissionPV: &api.PersistentVolume{
  300. ObjectMeta: metav1.ObjectMeta{
  301. Name: "awsebs", Namespace: "myns",
  302. Labels: map[string]string{
  303. v1.LabelZoneFailureDomain: "existingDomain",
  304. v1.LabelZoneRegion: "existingRegion",
  305. },
  306. },
  307. Spec: api.PersistentVolumeSpec{
  308. PersistentVolumeSource: api.PersistentVolumeSource{
  309. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  310. VolumeID: "123",
  311. },
  312. },
  313. },
  314. },
  315. postAdmissionPV: &api.PersistentVolume{
  316. ObjectMeta: metav1.ObjectMeta{
  317. Name: "awsebs",
  318. Namespace: "myns",
  319. Labels: map[string]string{
  320. v1.LabelZoneFailureDomain: "domain1",
  321. v1.LabelZoneRegion: "region1",
  322. },
  323. },
  324. Spec: api.PersistentVolumeSpec{
  325. PersistentVolumeSource: api.PersistentVolumeSource{
  326. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  327. VolumeID: "123",
  328. },
  329. },
  330. NodeAffinity: &api.VolumeNodeAffinity{
  331. Required: &api.NodeSelector{
  332. NodeSelectorTerms: []api.NodeSelectorTerm{
  333. {
  334. MatchExpressions: []api.NodeSelectorRequirement{
  335. {
  336. Key: v1.LabelZoneRegion,
  337. Operator: api.NodeSelectorOpIn,
  338. Values: []string{"region1"},
  339. },
  340. {
  341. Key: v1.LabelZoneFailureDomain,
  342. Operator: api.NodeSelectorOpIn,
  343. Values: []string{"domain1"},
  344. },
  345. },
  346. },
  347. },
  348. },
  349. },
  350. },
  351. },
  352. err: nil,
  353. },
  354. {
  355. name: "GCE PD PV labeled correctly",
  356. handler: newPersistentVolumeLabel(),
  357. pvlabeler: mockVolumeLabels(map[string]string{
  358. "a": "1",
  359. "b": "2",
  360. v1.LabelZoneFailureDomain: "1__2__3",
  361. }),
  362. preAdmissionPV: &api.PersistentVolume{
  363. ObjectMeta: metav1.ObjectMeta{Name: "gcepd", Namespace: "myns"},
  364. Spec: api.PersistentVolumeSpec{
  365. PersistentVolumeSource: api.PersistentVolumeSource{
  366. GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
  367. PDName: "123",
  368. },
  369. },
  370. },
  371. },
  372. postAdmissionPV: &api.PersistentVolume{
  373. ObjectMeta: metav1.ObjectMeta{
  374. Name: "gcepd",
  375. Namespace: "myns",
  376. Labels: map[string]string{
  377. "a": "1",
  378. "b": "2",
  379. v1.LabelZoneFailureDomain: "1__2__3",
  380. },
  381. },
  382. Spec: api.PersistentVolumeSpec{
  383. PersistentVolumeSource: api.PersistentVolumeSource{
  384. GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{
  385. PDName: "123",
  386. },
  387. },
  388. NodeAffinity: &api.VolumeNodeAffinity{
  389. Required: &api.NodeSelector{
  390. NodeSelectorTerms: []api.NodeSelectorTerm{
  391. {
  392. MatchExpressions: []api.NodeSelectorRequirement{
  393. {
  394. Key: "a",
  395. Operator: api.NodeSelectorOpIn,
  396. Values: []string{"1"},
  397. },
  398. {
  399. Key: "b",
  400. Operator: api.NodeSelectorOpIn,
  401. Values: []string{"2"},
  402. },
  403. {
  404. Key: v1.LabelZoneFailureDomain,
  405. Operator: api.NodeSelectorOpIn,
  406. Values: []string{"1", "2", "3"},
  407. },
  408. },
  409. },
  410. },
  411. },
  412. },
  413. },
  414. },
  415. err: nil,
  416. },
  417. {
  418. name: "Azure Disk PV labeled correctly",
  419. handler: newPersistentVolumeLabel(),
  420. pvlabeler: mockVolumeLabels(map[string]string{
  421. "a": "1",
  422. "b": "2",
  423. v1.LabelZoneFailureDomain: "1__2__3",
  424. }),
  425. preAdmissionPV: &api.PersistentVolume{
  426. ObjectMeta: metav1.ObjectMeta{
  427. Name: "azurepd",
  428. Namespace: "myns",
  429. },
  430. Spec: api.PersistentVolumeSpec{
  431. PersistentVolumeSource: api.PersistentVolumeSource{
  432. AzureDisk: &api.AzureDiskVolumeSource{
  433. DiskName: "123",
  434. },
  435. },
  436. },
  437. },
  438. postAdmissionPV: &api.PersistentVolume{
  439. ObjectMeta: metav1.ObjectMeta{
  440. Name: "azurepd",
  441. Namespace: "myns",
  442. Labels: map[string]string{
  443. "a": "1",
  444. "b": "2",
  445. v1.LabelZoneFailureDomain: "1__2__3",
  446. },
  447. },
  448. Spec: api.PersistentVolumeSpec{
  449. PersistentVolumeSource: api.PersistentVolumeSource{
  450. AzureDisk: &api.AzureDiskVolumeSource{
  451. DiskName: "123",
  452. },
  453. },
  454. NodeAffinity: &api.VolumeNodeAffinity{
  455. Required: &api.NodeSelector{
  456. NodeSelectorTerms: []api.NodeSelectorTerm{
  457. {
  458. MatchExpressions: []api.NodeSelectorRequirement{
  459. {
  460. Key: "a",
  461. Operator: api.NodeSelectorOpIn,
  462. Values: []string{"1"},
  463. },
  464. {
  465. Key: "b",
  466. Operator: api.NodeSelectorOpIn,
  467. Values: []string{"2"},
  468. },
  469. {
  470. Key: v1.LabelZoneFailureDomain,
  471. Operator: api.NodeSelectorOpIn,
  472. Values: []string{"1", "2", "3"},
  473. },
  474. },
  475. },
  476. },
  477. },
  478. },
  479. },
  480. },
  481. err: nil,
  482. },
  483. {
  484. name: "Cinder Disk PV labeled correctly",
  485. handler: newPersistentVolumeLabel(),
  486. pvlabeler: mockVolumeLabels(map[string]string{
  487. "a": "1",
  488. "b": "2",
  489. v1.LabelZoneFailureDomain: "1__2__3",
  490. }),
  491. preAdmissionPV: &api.PersistentVolume{
  492. ObjectMeta: metav1.ObjectMeta{
  493. Name: "azurepd",
  494. Namespace: "myns",
  495. },
  496. Spec: api.PersistentVolumeSpec{
  497. PersistentVolumeSource: api.PersistentVolumeSource{
  498. Cinder: &api.CinderPersistentVolumeSource{
  499. VolumeID: "123",
  500. },
  501. },
  502. },
  503. },
  504. postAdmissionPV: &api.PersistentVolume{
  505. ObjectMeta: metav1.ObjectMeta{
  506. Name: "azurepd",
  507. Namespace: "myns",
  508. Labels: map[string]string{
  509. "a": "1",
  510. "b": "2",
  511. v1.LabelZoneFailureDomain: "1__2__3",
  512. },
  513. },
  514. Spec: api.PersistentVolumeSpec{
  515. PersistentVolumeSource: api.PersistentVolumeSource{
  516. Cinder: &api.CinderPersistentVolumeSource{
  517. VolumeID: "123",
  518. },
  519. },
  520. NodeAffinity: &api.VolumeNodeAffinity{
  521. Required: &api.NodeSelector{
  522. NodeSelectorTerms: []api.NodeSelectorTerm{
  523. {
  524. MatchExpressions: []api.NodeSelectorRequirement{
  525. {
  526. Key: "a",
  527. Operator: api.NodeSelectorOpIn,
  528. Values: []string{"1"},
  529. },
  530. {
  531. Key: "b",
  532. Operator: api.NodeSelectorOpIn,
  533. Values: []string{"2"},
  534. },
  535. {
  536. Key: v1.LabelZoneFailureDomain,
  537. Operator: api.NodeSelectorOpIn,
  538. Values: []string{"1", "2", "3"},
  539. },
  540. },
  541. },
  542. },
  543. },
  544. },
  545. },
  546. },
  547. err: nil,
  548. },
  549. {
  550. name: "AWS EBS PV overrides user applied labels",
  551. handler: newPersistentVolumeLabel(),
  552. pvlabeler: mockVolumeLabels(map[string]string{
  553. "a": "1",
  554. "b": "2",
  555. v1.LabelZoneFailureDomain: "1__2__3",
  556. }),
  557. preAdmissionPV: &api.PersistentVolume{
  558. ObjectMeta: metav1.ObjectMeta{
  559. Name: "awsebs",
  560. Namespace: "myns",
  561. Labels: map[string]string{
  562. "a": "not1",
  563. },
  564. },
  565. Spec: api.PersistentVolumeSpec{
  566. PersistentVolumeSource: api.PersistentVolumeSource{
  567. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  568. VolumeID: "123",
  569. },
  570. },
  571. },
  572. },
  573. postAdmissionPV: &api.PersistentVolume{
  574. ObjectMeta: metav1.ObjectMeta{
  575. Name: "awsebs",
  576. Namespace: "myns",
  577. Labels: map[string]string{
  578. "a": "1",
  579. "b": "2",
  580. v1.LabelZoneFailureDomain: "1__2__3",
  581. },
  582. },
  583. Spec: api.PersistentVolumeSpec{
  584. PersistentVolumeSource: api.PersistentVolumeSource{
  585. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  586. VolumeID: "123",
  587. },
  588. },
  589. NodeAffinity: &api.VolumeNodeAffinity{
  590. Required: &api.NodeSelector{
  591. NodeSelectorTerms: []api.NodeSelectorTerm{
  592. {
  593. MatchExpressions: []api.NodeSelectorRequirement{
  594. {
  595. Key: "a",
  596. Operator: api.NodeSelectorOpIn,
  597. Values: []string{"1"},
  598. },
  599. {
  600. Key: "b",
  601. Operator: api.NodeSelectorOpIn,
  602. Values: []string{"2"},
  603. },
  604. {
  605. Key: v1.LabelZoneFailureDomain,
  606. Operator: api.NodeSelectorOpIn,
  607. Values: []string{"1", "2", "3"},
  608. },
  609. },
  610. },
  611. },
  612. },
  613. },
  614. },
  615. },
  616. err: nil,
  617. },
  618. {
  619. name: "AWS EBS PV conflicting affinity rules left in-tact",
  620. handler: newPersistentVolumeLabel(),
  621. pvlabeler: mockVolumeLabels(map[string]string{
  622. "a": "1",
  623. "b": "2",
  624. "c": "3",
  625. }),
  626. preAdmissionPV: &api.PersistentVolume{
  627. ObjectMeta: metav1.ObjectMeta{
  628. Name: "awsebs",
  629. Namespace: "myns",
  630. Labels: map[string]string{
  631. "c": "3",
  632. },
  633. },
  634. Spec: api.PersistentVolumeSpec{
  635. PersistentVolumeSource: api.PersistentVolumeSource{
  636. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  637. VolumeID: "123",
  638. },
  639. },
  640. NodeAffinity: &api.VolumeNodeAffinity{
  641. Required: &api.NodeSelector{
  642. NodeSelectorTerms: []api.NodeSelectorTerm{
  643. {
  644. MatchExpressions: []api.NodeSelectorRequirement{
  645. {
  646. Key: "c",
  647. Operator: api.NodeSelectorOpIn,
  648. Values: []string{"3"},
  649. },
  650. },
  651. },
  652. },
  653. },
  654. },
  655. },
  656. },
  657. postAdmissionPV: &api.PersistentVolume{
  658. ObjectMeta: metav1.ObjectMeta{
  659. Name: "awsebs",
  660. Namespace: "myns",
  661. Labels: map[string]string{
  662. "a": "1",
  663. "b": "2",
  664. "c": "3",
  665. },
  666. },
  667. Spec: api.PersistentVolumeSpec{
  668. PersistentVolumeSource: api.PersistentVolumeSource{
  669. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  670. VolumeID: "123",
  671. },
  672. },
  673. NodeAffinity: &api.VolumeNodeAffinity{
  674. Required: &api.NodeSelector{
  675. NodeSelectorTerms: []api.NodeSelectorTerm{
  676. {
  677. MatchExpressions: []api.NodeSelectorRequirement{
  678. {
  679. Key: "c",
  680. Operator: api.NodeSelectorOpIn,
  681. Values: []string{"3"},
  682. },
  683. },
  684. },
  685. },
  686. },
  687. },
  688. },
  689. },
  690. err: nil,
  691. },
  692. {
  693. name: "AWS EBS PV non-conflicting affinity rules added",
  694. handler: newPersistentVolumeLabel(),
  695. pvlabeler: mockVolumeLabels(map[string]string{
  696. "d": "1",
  697. "e": "2",
  698. "f": "3",
  699. }),
  700. preAdmissionPV: &api.PersistentVolume{
  701. ObjectMeta: metav1.ObjectMeta{
  702. Name: "awsebs",
  703. Namespace: "myns",
  704. Labels: map[string]string{
  705. "a": "1",
  706. "b": "2",
  707. "c": "3",
  708. },
  709. },
  710. Spec: api.PersistentVolumeSpec{
  711. PersistentVolumeSource: api.PersistentVolumeSource{
  712. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  713. VolumeID: "123",
  714. },
  715. },
  716. NodeAffinity: &api.VolumeNodeAffinity{
  717. Required: &api.NodeSelector{
  718. NodeSelectorTerms: []api.NodeSelectorTerm{
  719. {
  720. MatchExpressions: []api.NodeSelectorRequirement{
  721. {
  722. Key: "a",
  723. Operator: api.NodeSelectorOpIn,
  724. Values: []string{"1"},
  725. },
  726. {
  727. Key: "b",
  728. Operator: api.NodeSelectorOpIn,
  729. Values: []string{"2"},
  730. },
  731. {
  732. Key: "c",
  733. Operator: api.NodeSelectorOpIn,
  734. Values: []string{"3"},
  735. },
  736. },
  737. },
  738. },
  739. },
  740. },
  741. },
  742. },
  743. postAdmissionPV: &api.PersistentVolume{
  744. ObjectMeta: metav1.ObjectMeta{
  745. Name: "awsebs",
  746. Namespace: "myns",
  747. Labels: map[string]string{
  748. "a": "1",
  749. "b": "2",
  750. "c": "3",
  751. "d": "1",
  752. "e": "2",
  753. "f": "3",
  754. },
  755. },
  756. Spec: api.PersistentVolumeSpec{
  757. PersistentVolumeSource: api.PersistentVolumeSource{
  758. AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
  759. VolumeID: "123",
  760. },
  761. },
  762. NodeAffinity: &api.VolumeNodeAffinity{
  763. Required: &api.NodeSelector{
  764. NodeSelectorTerms: []api.NodeSelectorTerm{
  765. {
  766. MatchExpressions: []api.NodeSelectorRequirement{
  767. {
  768. Key: "a",
  769. Operator: api.NodeSelectorOpIn,
  770. Values: []string{"1"},
  771. },
  772. {
  773. Key: "b",
  774. Operator: api.NodeSelectorOpIn,
  775. Values: []string{"2"},
  776. },
  777. {
  778. Key: "c",
  779. Operator: api.NodeSelectorOpIn,
  780. Values: []string{"3"},
  781. },
  782. {
  783. Key: "d",
  784. Operator: api.NodeSelectorOpIn,
  785. Values: []string{"1"},
  786. },
  787. {
  788. Key: "e",
  789. Operator: api.NodeSelectorOpIn,
  790. Values: []string{"2"},
  791. },
  792. {
  793. Key: "f",
  794. Operator: api.NodeSelectorOpIn,
  795. Values: []string{"3"},
  796. },
  797. },
  798. },
  799. },
  800. },
  801. },
  802. },
  803. },
  804. err: nil,
  805. },
  806. {
  807. name: "vSphere PV labeled correctly",
  808. handler: newPersistentVolumeLabel(),
  809. pvlabeler: mockVolumeLabels(map[string]string{
  810. "a": "1",
  811. "b": "2",
  812. v1.LabelZoneFailureDomain: "1__2__3",
  813. }),
  814. preAdmissionPV: &api.PersistentVolume{
  815. ObjectMeta: metav1.ObjectMeta{
  816. Name: "vSpherePV",
  817. Namespace: "myns",
  818. },
  819. Spec: api.PersistentVolumeSpec{
  820. PersistentVolumeSource: api.PersistentVolumeSource{
  821. VsphereVolume: &api.VsphereVirtualDiskVolumeSource{
  822. VolumePath: "123",
  823. },
  824. },
  825. },
  826. },
  827. postAdmissionPV: &api.PersistentVolume{
  828. ObjectMeta: metav1.ObjectMeta{
  829. Name: "vSpherePV",
  830. Namespace: "myns",
  831. Labels: map[string]string{
  832. "a": "1",
  833. "b": "2",
  834. v1.LabelZoneFailureDomain: "1__2__3",
  835. },
  836. },
  837. Spec: api.PersistentVolumeSpec{
  838. PersistentVolumeSource: api.PersistentVolumeSource{
  839. VsphereVolume: &api.VsphereVirtualDiskVolumeSource{
  840. VolumePath: "123",
  841. },
  842. },
  843. NodeAffinity: &api.VolumeNodeAffinity{
  844. Required: &api.NodeSelector{
  845. NodeSelectorTerms: []api.NodeSelectorTerm{
  846. {
  847. MatchExpressions: []api.NodeSelectorRequirement{
  848. {
  849. Key: "a",
  850. Operator: api.NodeSelectorOpIn,
  851. Values: []string{"1"},
  852. },
  853. {
  854. Key: "b",
  855. Operator: api.NodeSelectorOpIn,
  856. Values: []string{"2"},
  857. },
  858. {
  859. Key: v1.LabelZoneFailureDomain,
  860. Operator: api.NodeSelectorOpIn,
  861. Values: []string{"1", "2", "3"},
  862. },
  863. },
  864. },
  865. },
  866. },
  867. },
  868. },
  869. },
  870. err: nil,
  871. },
  872. }
  873. for _, testcase := range testcases {
  874. t.Run(testcase.name, func(t *testing.T) {
  875. setPVLabeler(testcase.handler, testcase.pvlabeler)
  876. handler := admissiontesting.WithReinvocationTesting(t, admission.NewChainHandler(testcase.handler))
  877. err := handler.Admit(context.TODO(), admission.NewAttributesRecord(testcase.preAdmissionPV, nil, api.Kind("PersistentVolume").WithVersion("version"), testcase.preAdmissionPV.Namespace, testcase.preAdmissionPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
  878. if !reflect.DeepEqual(err, testcase.err) {
  879. t.Logf("expected error: %q", testcase.err)
  880. t.Logf("actual error: %q", err)
  881. t.Error("unexpected error when admitting PV")
  882. }
  883. // sort node selector match expression by key because they are added out of order in the admission controller
  884. sortMatchExpressions(testcase.preAdmissionPV)
  885. if !reflect.DeepEqual(testcase.preAdmissionPV, testcase.postAdmissionPV) {
  886. t.Logf("expected PV: %+v", testcase.postAdmissionPV)
  887. t.Logf("actual PV: %+v", testcase.preAdmissionPV)
  888. t.Error("unexpected PV")
  889. }
  890. })
  891. }
  892. }
  893. // setPVLabler applies the given mock pvlabeler to implement PV labeling for all cloud providers.
  894. // Given we mock out the values of the labels anyways, assigning the same mock labeler for every
  895. // provider does not reduce test coverage but it does simplify/clean up the tests here because
  896. // the provider is then decided based on the type of PV (EBS, Cinder, GCEPD, Azure Disk, etc)
  897. func setPVLabeler(handler *persistentVolumeLabel, pvlabeler cloudprovider.PVLabeler) {
  898. handler.awsPVLabeler = pvlabeler
  899. handler.gcePVLabeler = pvlabeler
  900. handler.azurePVLabeler = pvlabeler
  901. handler.openStackPVLabeler = pvlabeler
  902. handler.vspherePVLabeler = pvlabeler
  903. }
  904. // sortMatchExpressions sorts a PV's node selector match expressions by key name if it is not nil
  905. func sortMatchExpressions(pv *api.PersistentVolume) {
  906. if pv.Spec.NodeAffinity == nil ||
  907. pv.Spec.NodeAffinity.Required == nil ||
  908. pv.Spec.NodeAffinity.Required.NodeSelectorTerms == nil {
  909. return
  910. }
  911. match := pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions
  912. sort.Slice(match, func(i, j int) bool {
  913. return match[i].Key < match[j].Key
  914. })
  915. pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions = match
  916. }