validation_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. /*
  2. Copyright 2016 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 validation
  14. import (
  15. "strings"
  16. "testing"
  17. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  18. "k8s.io/apimachinery/pkg/types"
  19. utilfeature "k8s.io/apiserver/pkg/util/feature"
  20. featuregatetesting "k8s.io/component-base/featuregate/testing"
  21. "k8s.io/kubernetes/pkg/apis/batch"
  22. api "k8s.io/kubernetes/pkg/apis/core"
  23. "k8s.io/kubernetes/pkg/features"
  24. )
  25. func getValidManualSelector() *metav1.LabelSelector {
  26. return &metav1.LabelSelector{
  27. MatchLabels: map[string]string{"a": "b"},
  28. }
  29. }
  30. func getValidPodTemplateSpecForManual(selector *metav1.LabelSelector) api.PodTemplateSpec {
  31. return api.PodTemplateSpec{
  32. ObjectMeta: metav1.ObjectMeta{
  33. Labels: selector.MatchLabels,
  34. },
  35. Spec: api.PodSpec{
  36. RestartPolicy: api.RestartPolicyOnFailure,
  37. DNSPolicy: api.DNSClusterFirst,
  38. Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
  39. },
  40. }
  41. }
  42. func getValidGeneratedSelector() *metav1.LabelSelector {
  43. return &metav1.LabelSelector{
  44. MatchLabels: map[string]string{"controller-uid": "1a2b3c", "job-name": "myjob"},
  45. }
  46. }
  47. func getValidPodTemplateSpecForGenerated(selector *metav1.LabelSelector) api.PodTemplateSpec {
  48. return api.PodTemplateSpec{
  49. ObjectMeta: metav1.ObjectMeta{
  50. Labels: selector.MatchLabels,
  51. },
  52. Spec: api.PodSpec{
  53. RestartPolicy: api.RestartPolicyOnFailure,
  54. DNSPolicy: api.DNSClusterFirst,
  55. Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
  56. },
  57. }
  58. }
  59. func TestValidateJob(t *testing.T) {
  60. validManualSelector := getValidManualSelector()
  61. validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector)
  62. validGeneratedSelector := getValidGeneratedSelector()
  63. validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector)
  64. successCases := map[string]batch.Job{
  65. "manual selector": {
  66. ObjectMeta: metav1.ObjectMeta{
  67. Name: "myjob",
  68. Namespace: metav1.NamespaceDefault,
  69. UID: types.UID("1a2b3c"),
  70. },
  71. Spec: batch.JobSpec{
  72. Selector: validManualSelector,
  73. ManualSelector: newBool(true),
  74. Template: validPodTemplateSpecForManual,
  75. },
  76. },
  77. "generated selector": {
  78. ObjectMeta: metav1.ObjectMeta{
  79. Name: "myjob",
  80. Namespace: metav1.NamespaceDefault,
  81. UID: types.UID("1a2b3c"),
  82. },
  83. Spec: batch.JobSpec{
  84. Selector: validGeneratedSelector,
  85. Template: validPodTemplateSpecForGenerated,
  86. },
  87. },
  88. }
  89. for k, v := range successCases {
  90. if errs := ValidateJob(&v); len(errs) != 0 {
  91. t.Errorf("expected success for %s: %v", k, errs)
  92. }
  93. }
  94. negative := int32(-1)
  95. negative64 := int64(-1)
  96. errorCases := map[string]batch.Job{
  97. "spec.parallelism:must be greater than or equal to 0": {
  98. ObjectMeta: metav1.ObjectMeta{
  99. Name: "myjob",
  100. Namespace: metav1.NamespaceDefault,
  101. UID: types.UID("1a2b3c"),
  102. },
  103. Spec: batch.JobSpec{
  104. Parallelism: &negative,
  105. Selector: validGeneratedSelector,
  106. Template: validPodTemplateSpecForGenerated,
  107. },
  108. },
  109. "spec.completions:must be greater than or equal to 0": {
  110. ObjectMeta: metav1.ObjectMeta{
  111. Name: "myjob",
  112. Namespace: metav1.NamespaceDefault,
  113. UID: types.UID("1a2b3c"),
  114. },
  115. Spec: batch.JobSpec{
  116. Completions: &negative,
  117. Selector: validGeneratedSelector,
  118. Template: validPodTemplateSpecForGenerated,
  119. },
  120. },
  121. "spec.activeDeadlineSeconds:must be greater than or equal to 0": {
  122. ObjectMeta: metav1.ObjectMeta{
  123. Name: "myjob",
  124. Namespace: metav1.NamespaceDefault,
  125. UID: types.UID("1a2b3c"),
  126. },
  127. Spec: batch.JobSpec{
  128. ActiveDeadlineSeconds: &negative64,
  129. Selector: validGeneratedSelector,
  130. Template: validPodTemplateSpecForGenerated,
  131. },
  132. },
  133. "spec.selector:Required value": {
  134. ObjectMeta: metav1.ObjectMeta{
  135. Name: "myjob",
  136. Namespace: metav1.NamespaceDefault,
  137. UID: types.UID("1a2b3c"),
  138. },
  139. Spec: batch.JobSpec{
  140. Template: validPodTemplateSpecForGenerated,
  141. },
  142. },
  143. "spec.template.metadata.labels: Invalid value: {\"y\":\"z\"}: `selector` does not match template `labels`": {
  144. ObjectMeta: metav1.ObjectMeta{
  145. Name: "myjob",
  146. Namespace: metav1.NamespaceDefault,
  147. UID: types.UID("1a2b3c"),
  148. },
  149. Spec: batch.JobSpec{
  150. Selector: validManualSelector,
  151. ManualSelector: newBool(true),
  152. Template: api.PodTemplateSpec{
  153. ObjectMeta: metav1.ObjectMeta{
  154. Labels: map[string]string{"y": "z"},
  155. },
  156. Spec: api.PodSpec{
  157. RestartPolicy: api.RestartPolicyOnFailure,
  158. DNSPolicy: api.DNSClusterFirst,
  159. Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
  160. },
  161. },
  162. },
  163. },
  164. "spec.template.metadata.labels: Invalid value: {\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": {
  165. ObjectMeta: metav1.ObjectMeta{
  166. Name: "myjob",
  167. Namespace: metav1.NamespaceDefault,
  168. UID: types.UID("1a2b3c"),
  169. },
  170. Spec: batch.JobSpec{
  171. Selector: validManualSelector,
  172. ManualSelector: newBool(true),
  173. Template: api.PodTemplateSpec{
  174. ObjectMeta: metav1.ObjectMeta{
  175. Labels: map[string]string{"controller-uid": "4d5e6f"},
  176. },
  177. Spec: api.PodSpec{
  178. RestartPolicy: api.RestartPolicyOnFailure,
  179. DNSPolicy: api.DNSClusterFirst,
  180. Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
  181. },
  182. },
  183. },
  184. },
  185. "spec.template.spec.restartPolicy: Unsupported value": {
  186. ObjectMeta: metav1.ObjectMeta{
  187. Name: "myjob",
  188. Namespace: metav1.NamespaceDefault,
  189. UID: types.UID("1a2b3c"),
  190. },
  191. Spec: batch.JobSpec{
  192. Selector: validManualSelector,
  193. ManualSelector: newBool(true),
  194. Template: api.PodTemplateSpec{
  195. ObjectMeta: metav1.ObjectMeta{
  196. Labels: validManualSelector.MatchLabels,
  197. },
  198. Spec: api.PodSpec{
  199. RestartPolicy: api.RestartPolicyAlways,
  200. DNSPolicy: api.DNSClusterFirst,
  201. Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
  202. },
  203. },
  204. },
  205. },
  206. }
  207. for _, setFeature := range []bool{true, false} {
  208. defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TTLAfterFinished, setFeature)()
  209. ttlCase := "spec.ttlSecondsAfterFinished:must be greater than or equal to 0"
  210. if utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
  211. errorCases[ttlCase] = batch.Job{
  212. ObjectMeta: metav1.ObjectMeta{
  213. Name: "myjob",
  214. Namespace: metav1.NamespaceDefault,
  215. UID: types.UID("1a2b3c"),
  216. },
  217. Spec: batch.JobSpec{
  218. TTLSecondsAfterFinished: &negative,
  219. Selector: validGeneratedSelector,
  220. Template: validPodTemplateSpecForGenerated,
  221. },
  222. }
  223. } else {
  224. delete(errorCases, ttlCase)
  225. }
  226. for k, v := range errorCases {
  227. errs := ValidateJob(&v)
  228. if len(errs) == 0 {
  229. t.Errorf("expected failure for %s", k)
  230. } else {
  231. s := strings.Split(k, ":")
  232. err := errs[0]
  233. if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
  234. t.Errorf("unexpected error: %v, expected: %s", err, k)
  235. }
  236. }
  237. }
  238. }
  239. }
  240. func TestValidateJobUpdateStatus(t *testing.T) {
  241. type testcase struct {
  242. old batch.Job
  243. update batch.Job
  244. }
  245. successCases := []testcase{
  246. {
  247. old: batch.Job{
  248. ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
  249. Status: batch.JobStatus{
  250. Active: 1,
  251. Succeeded: 2,
  252. Failed: 3,
  253. },
  254. },
  255. update: batch.Job{
  256. ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
  257. Status: batch.JobStatus{
  258. Active: 1,
  259. Succeeded: 1,
  260. Failed: 3,
  261. },
  262. },
  263. },
  264. }
  265. for _, successCase := range successCases {
  266. successCase.old.ObjectMeta.ResourceVersion = "1"
  267. successCase.update.ObjectMeta.ResourceVersion = "1"
  268. if errs := ValidateJobUpdateStatus(&successCase.update, &successCase.old); len(errs) != 0 {
  269. t.Errorf("expected success: %v", errs)
  270. }
  271. }
  272. errorCases := map[string]testcase{
  273. "[status.active: Invalid value: -1: must be greater than or equal to 0, status.succeeded: Invalid value: -2: must be greater than or equal to 0]": {
  274. old: batch.Job{
  275. ObjectMeta: metav1.ObjectMeta{
  276. Name: "abc",
  277. Namespace: metav1.NamespaceDefault,
  278. ResourceVersion: "10",
  279. },
  280. Status: batch.JobStatus{
  281. Active: 1,
  282. Succeeded: 2,
  283. Failed: 3,
  284. },
  285. },
  286. update: batch.Job{
  287. ObjectMeta: metav1.ObjectMeta{
  288. Name: "abc",
  289. Namespace: metav1.NamespaceDefault,
  290. ResourceVersion: "10",
  291. },
  292. Status: batch.JobStatus{
  293. Active: -1,
  294. Succeeded: -2,
  295. Failed: 3,
  296. },
  297. },
  298. },
  299. }
  300. for testName, errorCase := range errorCases {
  301. errs := ValidateJobUpdateStatus(&errorCase.update, &errorCase.old)
  302. if len(errs) == 0 {
  303. t.Errorf("expected failure: %s", testName)
  304. continue
  305. }
  306. if errs.ToAggregate().Error() != testName {
  307. t.Errorf("expected '%s' got '%s'", errs.ToAggregate().Error(), testName)
  308. }
  309. }
  310. }
  311. func TestValidateCronJob(t *testing.T) {
  312. validManualSelector := getValidManualSelector()
  313. validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector())
  314. validPodTemplateSpec.Labels = map[string]string{}
  315. successCases := map[string]batch.CronJob{
  316. "basic scheduled job": {
  317. ObjectMeta: metav1.ObjectMeta{
  318. Name: "mycronjob",
  319. Namespace: metav1.NamespaceDefault,
  320. UID: types.UID("1a2b3c"),
  321. },
  322. Spec: batch.CronJobSpec{
  323. Schedule: "* * * * ?",
  324. ConcurrencyPolicy: batch.AllowConcurrent,
  325. JobTemplate: batch.JobTemplateSpec{
  326. Spec: batch.JobSpec{
  327. Template: validPodTemplateSpec,
  328. },
  329. },
  330. },
  331. },
  332. "non-standard scheduled": {
  333. ObjectMeta: metav1.ObjectMeta{
  334. Name: "mycronjob",
  335. Namespace: metav1.NamespaceDefault,
  336. UID: types.UID("1a2b3c"),
  337. },
  338. Spec: batch.CronJobSpec{
  339. Schedule: "@hourly",
  340. ConcurrencyPolicy: batch.AllowConcurrent,
  341. JobTemplate: batch.JobTemplateSpec{
  342. Spec: batch.JobSpec{
  343. Template: validPodTemplateSpec,
  344. },
  345. },
  346. },
  347. },
  348. }
  349. for k, v := range successCases {
  350. if errs := ValidateCronJob(&v); len(errs) != 0 {
  351. t.Errorf("expected success for %s: %v", k, errs)
  352. }
  353. // Update validation should pass same success cases
  354. // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update
  355. v = *v.DeepCopy()
  356. v.ResourceVersion = "1"
  357. if errs := ValidateCronJobUpdate(&v, &v); len(errs) != 0 {
  358. t.Errorf("expected success for %s: %v", k, errs)
  359. }
  360. }
  361. negative := int32(-1)
  362. negative64 := int64(-1)
  363. errorCases := map[string]batch.CronJob{
  364. "spec.schedule: Invalid value": {
  365. ObjectMeta: metav1.ObjectMeta{
  366. Name: "mycronjob",
  367. Namespace: metav1.NamespaceDefault,
  368. UID: types.UID("1a2b3c"),
  369. },
  370. Spec: batch.CronJobSpec{
  371. Schedule: "error",
  372. ConcurrencyPolicy: batch.AllowConcurrent,
  373. JobTemplate: batch.JobTemplateSpec{
  374. Spec: batch.JobSpec{
  375. Template: validPodTemplateSpec,
  376. },
  377. },
  378. },
  379. },
  380. "spec.schedule: Required value": {
  381. ObjectMeta: metav1.ObjectMeta{
  382. Name: "mycronjob",
  383. Namespace: metav1.NamespaceDefault,
  384. UID: types.UID("1a2b3c"),
  385. },
  386. Spec: batch.CronJobSpec{
  387. Schedule: "",
  388. ConcurrencyPolicy: batch.AllowConcurrent,
  389. JobTemplate: batch.JobTemplateSpec{
  390. Spec: batch.JobSpec{
  391. Template: validPodTemplateSpec,
  392. },
  393. },
  394. },
  395. },
  396. "spec.startingDeadlineSeconds:must be greater than or equal to 0": {
  397. ObjectMeta: metav1.ObjectMeta{
  398. Name: "mycronjob",
  399. Namespace: metav1.NamespaceDefault,
  400. UID: types.UID("1a2b3c"),
  401. },
  402. Spec: batch.CronJobSpec{
  403. Schedule: "* * * * ?",
  404. ConcurrencyPolicy: batch.AllowConcurrent,
  405. StartingDeadlineSeconds: &negative64,
  406. JobTemplate: batch.JobTemplateSpec{
  407. Spec: batch.JobSpec{
  408. Template: validPodTemplateSpec,
  409. },
  410. },
  411. },
  412. },
  413. "spec.successfulJobsHistoryLimit: must be greater than or equal to 0": {
  414. ObjectMeta: metav1.ObjectMeta{
  415. Name: "mycronjob",
  416. Namespace: metav1.NamespaceDefault,
  417. UID: types.UID("1a2b3c"),
  418. },
  419. Spec: batch.CronJobSpec{
  420. Schedule: "* * * * ?",
  421. ConcurrencyPolicy: batch.AllowConcurrent,
  422. SuccessfulJobsHistoryLimit: &negative,
  423. JobTemplate: batch.JobTemplateSpec{
  424. Spec: batch.JobSpec{
  425. Template: validPodTemplateSpec,
  426. },
  427. },
  428. },
  429. },
  430. "spec.failedJobsHistoryLimit: must be greater than or equal to 0": {
  431. ObjectMeta: metav1.ObjectMeta{
  432. Name: "mycronjob",
  433. Namespace: metav1.NamespaceDefault,
  434. UID: types.UID("1a2b3c"),
  435. },
  436. Spec: batch.CronJobSpec{
  437. Schedule: "* * * * ?",
  438. ConcurrencyPolicy: batch.AllowConcurrent,
  439. FailedJobsHistoryLimit: &negative,
  440. JobTemplate: batch.JobTemplateSpec{
  441. Spec: batch.JobSpec{
  442. Template: validPodTemplateSpec,
  443. },
  444. },
  445. },
  446. },
  447. "spec.concurrencyPolicy: Required value": {
  448. ObjectMeta: metav1.ObjectMeta{
  449. Name: "mycronjob",
  450. Namespace: metav1.NamespaceDefault,
  451. UID: types.UID("1a2b3c"),
  452. },
  453. Spec: batch.CronJobSpec{
  454. Schedule: "* * * * ?",
  455. JobTemplate: batch.JobTemplateSpec{
  456. Spec: batch.JobSpec{
  457. Template: validPodTemplateSpec,
  458. },
  459. },
  460. },
  461. },
  462. "spec.jobTemplate.spec.parallelism:must be greater than or equal to 0": {
  463. ObjectMeta: metav1.ObjectMeta{
  464. Name: "mycronjob",
  465. Namespace: metav1.NamespaceDefault,
  466. UID: types.UID("1a2b3c"),
  467. },
  468. Spec: batch.CronJobSpec{
  469. Schedule: "* * * * ?",
  470. ConcurrencyPolicy: batch.AllowConcurrent,
  471. JobTemplate: batch.JobTemplateSpec{
  472. Spec: batch.JobSpec{
  473. Parallelism: &negative,
  474. Template: validPodTemplateSpec,
  475. },
  476. },
  477. },
  478. },
  479. "spec.jobTemplate.spec.completions:must be greater than or equal to 0": {
  480. ObjectMeta: metav1.ObjectMeta{
  481. Name: "mycronjob",
  482. Namespace: metav1.NamespaceDefault,
  483. UID: types.UID("1a2b3c"),
  484. },
  485. Spec: batch.CronJobSpec{
  486. Schedule: "* * * * ?",
  487. ConcurrencyPolicy: batch.AllowConcurrent,
  488. JobTemplate: batch.JobTemplateSpec{
  489. Spec: batch.JobSpec{
  490. Completions: &negative,
  491. Template: validPodTemplateSpec,
  492. },
  493. },
  494. },
  495. },
  496. "spec.jobTemplate.spec.activeDeadlineSeconds:must be greater than or equal to 0": {
  497. ObjectMeta: metav1.ObjectMeta{
  498. Name: "mycronjob",
  499. Namespace: metav1.NamespaceDefault,
  500. UID: types.UID("1a2b3c"),
  501. },
  502. Spec: batch.CronJobSpec{
  503. Schedule: "* * * * ?",
  504. ConcurrencyPolicy: batch.AllowConcurrent,
  505. JobTemplate: batch.JobTemplateSpec{
  506. Spec: batch.JobSpec{
  507. ActiveDeadlineSeconds: &negative64,
  508. Template: validPodTemplateSpec,
  509. },
  510. },
  511. },
  512. },
  513. "spec.jobTemplate.spec.selector: Invalid value: {\"matchLabels\":{\"a\":\"b\"}}: `selector` will be auto-generated": {
  514. ObjectMeta: metav1.ObjectMeta{
  515. Name: "mycronjob",
  516. Namespace: metav1.NamespaceDefault,
  517. UID: types.UID("1a2b3c"),
  518. },
  519. Spec: batch.CronJobSpec{
  520. Schedule: "* * * * ?",
  521. ConcurrencyPolicy: batch.AllowConcurrent,
  522. JobTemplate: batch.JobTemplateSpec{
  523. Spec: batch.JobSpec{
  524. Selector: validManualSelector,
  525. Template: validPodTemplateSpec,
  526. },
  527. },
  528. },
  529. },
  530. "metadata.name: must be no more than 52 characters": {
  531. ObjectMeta: metav1.ObjectMeta{
  532. Name: "10000000002000000000300000000040000000005000000000123",
  533. Namespace: metav1.NamespaceDefault,
  534. UID: types.UID("1a2b3c"),
  535. },
  536. Spec: batch.CronJobSpec{
  537. Schedule: "* * * * ?",
  538. ConcurrencyPolicy: batch.AllowConcurrent,
  539. JobTemplate: batch.JobTemplateSpec{
  540. Spec: batch.JobSpec{
  541. Template: validPodTemplateSpec,
  542. },
  543. },
  544. },
  545. },
  546. "spec.jobTemplate.spec.manualSelector: Unsupported value": {
  547. ObjectMeta: metav1.ObjectMeta{
  548. Name: "mycronjob",
  549. Namespace: metav1.NamespaceDefault,
  550. UID: types.UID("1a2b3c"),
  551. },
  552. Spec: batch.CronJobSpec{
  553. Schedule: "* * * * ?",
  554. ConcurrencyPolicy: batch.AllowConcurrent,
  555. JobTemplate: batch.JobTemplateSpec{
  556. Spec: batch.JobSpec{
  557. ManualSelector: newBool(true),
  558. Template: validPodTemplateSpec,
  559. },
  560. },
  561. },
  562. },
  563. "spec.jobTemplate.spec.template.spec.restartPolicy: Unsupported value": {
  564. ObjectMeta: metav1.ObjectMeta{
  565. Name: "mycronjob",
  566. Namespace: metav1.NamespaceDefault,
  567. UID: types.UID("1a2b3c"),
  568. },
  569. Spec: batch.CronJobSpec{
  570. Schedule: "* * * * ?",
  571. ConcurrencyPolicy: batch.AllowConcurrent,
  572. JobTemplate: batch.JobTemplateSpec{
  573. Spec: batch.JobSpec{
  574. Template: api.PodTemplateSpec{
  575. Spec: api.PodSpec{
  576. RestartPolicy: api.RestartPolicyAlways,
  577. DNSPolicy: api.DNSClusterFirst,
  578. Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
  579. },
  580. },
  581. },
  582. },
  583. },
  584. },
  585. }
  586. if utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
  587. errorCases["spec.jobTemplate.spec.ttlSecondsAfterFinished:must be greater than or equal to 0"] = batch.CronJob{
  588. ObjectMeta: metav1.ObjectMeta{
  589. Name: "mycronjob",
  590. Namespace: metav1.NamespaceDefault,
  591. UID: types.UID("1a2b3c"),
  592. },
  593. Spec: batch.CronJobSpec{
  594. Schedule: "* * * * ?",
  595. ConcurrencyPolicy: batch.AllowConcurrent,
  596. JobTemplate: batch.JobTemplateSpec{
  597. Spec: batch.JobSpec{
  598. TTLSecondsAfterFinished: &negative,
  599. Template: validPodTemplateSpec,
  600. },
  601. },
  602. },
  603. }
  604. }
  605. for k, v := range errorCases {
  606. errs := ValidateCronJob(&v)
  607. if len(errs) == 0 {
  608. t.Errorf("expected failure for %s", k)
  609. } else {
  610. s := strings.Split(k, ":")
  611. err := errs[0]
  612. if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
  613. t.Errorf("unexpected error: %v, expected: %s", err, k)
  614. }
  615. }
  616. // Update validation should fail all failure cases other than the 52 character name limit
  617. // copy to avoid polluting the testcase object, set a resourceVersion to allow validating update, and test a no-op update
  618. v = *v.DeepCopy()
  619. v.ResourceVersion = "1"
  620. errs = ValidateCronJobUpdate(&v, &v)
  621. if len(errs) == 0 {
  622. if k == "metadata.name: must be no more than 52 characters" {
  623. continue
  624. }
  625. t.Errorf("expected failure for %s", k)
  626. } else {
  627. s := strings.Split(k, ":")
  628. err := errs[0]
  629. if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
  630. t.Errorf("unexpected error: %v, expected: %s", err, k)
  631. }
  632. }
  633. }
  634. }
  635. func newBool(val bool) *bool {
  636. p := new(bool)
  637. *p = val
  638. return p
  639. }