cronjob_controller_test.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  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 cronjob
  14. import (
  15. "strconv"
  16. "strings"
  17. "testing"
  18. "time"
  19. batchv1 "k8s.io/api/batch/v1"
  20. batchV1beta1 "k8s.io/api/batch/v1beta1"
  21. "k8s.io/api/core/v1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/types"
  24. "k8s.io/apimachinery/pkg/util/sets"
  25. "k8s.io/client-go/tools/record"
  26. // For the cronjob controller to do conversions.
  27. _ "k8s.io/kubernetes/pkg/apis/batch/install"
  28. _ "k8s.io/kubernetes/pkg/apis/core/install"
  29. )
  30. var (
  31. // schedule is hourly on the hour
  32. onTheHour = "0 * * * ?"
  33. errorSchedule = "obvious error schedule"
  34. )
  35. func justBeforeTheHour() time.Time {
  36. T1, err := time.Parse(time.RFC3339, "2016-05-19T09:59:00Z")
  37. if err != nil {
  38. panic("test setup error")
  39. }
  40. return T1
  41. }
  42. func topOfTheHour() time.Time {
  43. T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
  44. if err != nil {
  45. panic("test setup error")
  46. }
  47. return T1
  48. }
  49. func justAfterTheHour() time.Time {
  50. T1, err := time.Parse(time.RFC3339, "2016-05-19T10:01:00Z")
  51. if err != nil {
  52. panic("test setup error")
  53. }
  54. return T1
  55. }
  56. func weekAfterTheHour() time.Time {
  57. T1, err := time.Parse(time.RFC3339, "2016-05-26T10:00:00Z")
  58. if err != nil {
  59. panic("test setup error")
  60. }
  61. return T1
  62. }
  63. func justBeforeThePriorHour() time.Time {
  64. T1, err := time.Parse(time.RFC3339, "2016-05-19T08:59:00Z")
  65. if err != nil {
  66. panic("test setup error")
  67. }
  68. return T1
  69. }
  70. func justAfterThePriorHour() time.Time {
  71. T1, err := time.Parse(time.RFC3339, "2016-05-19T09:01:00Z")
  72. if err != nil {
  73. panic("test setup error")
  74. }
  75. return T1
  76. }
  77. func startTimeStringToTime(startTime string) time.Time {
  78. T1, err := time.Parse(time.RFC3339, startTime)
  79. if err != nil {
  80. panic("test setup error")
  81. }
  82. return T1
  83. }
  84. // returns a cronJob with some fields filled in.
  85. func cronJob() batchV1beta1.CronJob {
  86. return batchV1beta1.CronJob{
  87. ObjectMeta: metav1.ObjectMeta{
  88. Name: "mycronjob",
  89. Namespace: "snazzycats",
  90. UID: types.UID("1a2b3c"),
  91. SelfLink: "/apis/batch/v1beta1/namespaces/snazzycats/cronjobs/mycronjob",
  92. CreationTimestamp: metav1.Time{Time: justBeforeTheHour()},
  93. },
  94. Spec: batchV1beta1.CronJobSpec{
  95. Schedule: "* * * * ?",
  96. ConcurrencyPolicy: batchV1beta1.AllowConcurrent,
  97. JobTemplate: batchV1beta1.JobTemplateSpec{
  98. ObjectMeta: metav1.ObjectMeta{
  99. Labels: map[string]string{"a": "b"},
  100. Annotations: map[string]string{"x": "y"},
  101. },
  102. Spec: jobSpec(),
  103. },
  104. },
  105. }
  106. }
  107. func jobSpec() batchv1.JobSpec {
  108. one := int32(1)
  109. return batchv1.JobSpec{
  110. Parallelism: &one,
  111. Completions: &one,
  112. Template: v1.PodTemplateSpec{
  113. ObjectMeta: metav1.ObjectMeta{
  114. Labels: map[string]string{
  115. "foo": "bar",
  116. },
  117. },
  118. Spec: v1.PodSpec{
  119. Containers: []v1.Container{
  120. {Image: "foo/bar"},
  121. },
  122. },
  123. },
  124. }
  125. }
  126. func newJob(UID string) batchv1.Job {
  127. return batchv1.Job{
  128. ObjectMeta: metav1.ObjectMeta{
  129. UID: types.UID(UID),
  130. Name: "foobar",
  131. Namespace: metav1.NamespaceDefault,
  132. SelfLink: "/apis/batch/v1/namespaces/snazzycats/jobs/myjob",
  133. },
  134. Spec: jobSpec(),
  135. }
  136. }
  137. var (
  138. shortDead int64 = 10
  139. mediumDead int64 = 2 * 60 * 60
  140. longDead int64 = 1000000
  141. noDead int64 = -12345
  142. A = batchV1beta1.AllowConcurrent
  143. f = batchV1beta1.ForbidConcurrent
  144. R = batchV1beta1.ReplaceConcurrent
  145. T = true
  146. F = false
  147. )
  148. func TestSyncOne_RunOrNot(t *testing.T) {
  149. // Check expectations on deadline parameters
  150. if shortDead/60/60 >= 1 {
  151. t.Errorf("shortDead should be less than one hour")
  152. }
  153. if mediumDead/60/60 < 1 || mediumDead/60/60 >= 24 {
  154. t.Errorf("mediumDead should be between one hour and one day")
  155. }
  156. if longDead/60/60/24 < 10 {
  157. t.Errorf("longDead should be at least ten days")
  158. }
  159. testCases := map[string]struct {
  160. // sj spec
  161. concurrencyPolicy batchV1beta1.ConcurrencyPolicy
  162. suspend bool
  163. schedule string
  164. deadline int64
  165. // sj status
  166. ranPreviously bool
  167. stillActive bool
  168. // environment
  169. now time.Time
  170. // expectations
  171. expectCreate bool
  172. expectDelete bool
  173. expectActive int
  174. expectedWarnings int
  175. }{
  176. "never ran, not valid schedule, A": {A, F, errorSchedule, noDead, F, F, justBeforeTheHour(), F, F, 0, 1},
  177. "never ran, not valid schedule, F": {f, F, errorSchedule, noDead, F, F, justBeforeTheHour(), F, F, 0, 1},
  178. "never ran, not valid schedule, R": {f, F, errorSchedule, noDead, F, F, justBeforeTheHour(), F, F, 0, 1},
  179. "never ran, not time, A": {A, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, 0, 0},
  180. "never ran, not time, F": {f, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, 0, 0},
  181. "never ran, not time, R": {R, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, 0, 0},
  182. "never ran, is time, A": {A, F, onTheHour, noDead, F, F, justAfterTheHour(), T, F, 1, 0},
  183. "never ran, is time, F": {f, F, onTheHour, noDead, F, F, justAfterTheHour(), T, F, 1, 0},
  184. "never ran, is time, R": {R, F, onTheHour, noDead, F, F, justAfterTheHour(), T, F, 1, 0},
  185. "never ran, is time, suspended": {A, T, onTheHour, noDead, F, F, justAfterTheHour(), F, F, 0, 0},
  186. "never ran, is time, past deadline": {A, F, onTheHour, shortDead, F, F, justAfterTheHour(), F, F, 0, 0},
  187. "never ran, is time, not past deadline": {A, F, onTheHour, longDead, F, F, justAfterTheHour(), T, F, 1, 0},
  188. "prev ran but done, not time, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F, 0, 0},
  189. "prev ran but done, not time, F": {f, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F, 0, 0},
  190. "prev ran but done, not time, R": {R, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F, 0, 0},
  191. "prev ran but done, is time, A": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F, 1, 0},
  192. "prev ran but done, is time, F": {f, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F, 1, 0},
  193. "prev ran but done, is time, R": {R, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F, 1, 0},
  194. "prev ran but done, is time, suspended": {A, T, onTheHour, noDead, T, F, justAfterTheHour(), F, F, 0, 0},
  195. "prev ran but done, is time, past deadline": {A, F, onTheHour, shortDead, T, F, justAfterTheHour(), F, F, 0, 0},
  196. "prev ran but done, is time, not past deadline": {A, F, onTheHour, longDead, T, F, justAfterTheHour(), T, F, 1, 0},
  197. "still active, not time, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, 1, 0},
  198. "still active, not time, F": {f, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, 1, 0},
  199. "still active, not time, R": {R, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, 1, 0},
  200. "still active, is time, A": {A, F, onTheHour, noDead, T, T, justAfterTheHour(), T, F, 2, 0},
  201. "still active, is time, F": {f, F, onTheHour, noDead, T, T, justAfterTheHour(), F, F, 1, 0},
  202. "still active, is time, R": {R, F, onTheHour, noDead, T, T, justAfterTheHour(), T, T, 1, 0},
  203. "still active, is time, suspended": {A, T, onTheHour, noDead, T, T, justAfterTheHour(), F, F, 1, 0},
  204. "still active, is time, past deadline": {A, F, onTheHour, shortDead, T, T, justAfterTheHour(), F, F, 1, 0},
  205. "still active, is time, not past deadline": {A, F, onTheHour, longDead, T, T, justAfterTheHour(), T, F, 2, 0},
  206. // Controller should fail to schedule these, as there are too many missed starting times
  207. // and either no deadline or a too long deadline.
  208. "prev ran but done, long overdue, not past deadline, A": {A, F, onTheHour, longDead, T, F, weekAfterTheHour(), F, F, 0, 1},
  209. "prev ran but done, long overdue, not past deadline, R": {R, F, onTheHour, longDead, T, F, weekAfterTheHour(), F, F, 0, 1},
  210. "prev ran but done, long overdue, not past deadline, F": {f, F, onTheHour, longDead, T, F, weekAfterTheHour(), F, F, 0, 1},
  211. "prev ran but done, long overdue, no deadline, A": {A, F, onTheHour, noDead, T, F, weekAfterTheHour(), F, F, 0, 1},
  212. "prev ran but done, long overdue, no deadline, R": {R, F, onTheHour, noDead, T, F, weekAfterTheHour(), F, F, 0, 1},
  213. "prev ran but done, long overdue, no deadline, F": {f, F, onTheHour, noDead, T, F, weekAfterTheHour(), F, F, 0, 1},
  214. "prev ran but done, long overdue, past medium deadline, A": {A, F, onTheHour, mediumDead, T, F, weekAfterTheHour(), T, F, 1, 0},
  215. "prev ran but done, long overdue, past short deadline, A": {A, F, onTheHour, shortDead, T, F, weekAfterTheHour(), T, F, 1, 0},
  216. "prev ran but done, long overdue, past medium deadline, R": {R, F, onTheHour, mediumDead, T, F, weekAfterTheHour(), T, F, 1, 0},
  217. "prev ran but done, long overdue, past short deadline, R": {R, F, onTheHour, shortDead, T, F, weekAfterTheHour(), T, F, 1, 0},
  218. "prev ran but done, long overdue, past medium deadline, F": {f, F, onTheHour, mediumDead, T, F, weekAfterTheHour(), T, F, 1, 0},
  219. "prev ran but done, long overdue, past short deadline, F": {f, F, onTheHour, shortDead, T, F, weekAfterTheHour(), T, F, 1, 0},
  220. }
  221. for name, tc := range testCases {
  222. sj := cronJob()
  223. sj.Spec.ConcurrencyPolicy = tc.concurrencyPolicy
  224. sj.Spec.Suspend = &tc.suspend
  225. sj.Spec.Schedule = tc.schedule
  226. if tc.deadline != noDead {
  227. sj.Spec.StartingDeadlineSeconds = &tc.deadline
  228. }
  229. var (
  230. job *batchv1.Job
  231. err error
  232. )
  233. js := []batchv1.Job{}
  234. if tc.ranPreviously {
  235. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeThePriorHour()}
  236. sj.Status.LastScheduleTime = &metav1.Time{Time: justAfterThePriorHour()}
  237. job, err = getJobFromTemplate(&sj, sj.Status.LastScheduleTime.Time)
  238. if err != nil {
  239. t.Fatalf("%s: nexpected error creating a job from template: %v", name, err)
  240. }
  241. job.UID = "1234"
  242. job.Namespace = ""
  243. if tc.stillActive {
  244. sj.Status.Active = []v1.ObjectReference{{UID: job.UID}}
  245. js = append(js, *job)
  246. }
  247. } else {
  248. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeTheHour()}
  249. if tc.stillActive {
  250. t.Errorf("%s: test setup error: this case makes no sense", name)
  251. }
  252. }
  253. jc := &fakeJobControl{Job: job}
  254. sjc := &fakeSJControl{}
  255. recorder := record.NewFakeRecorder(10)
  256. syncOne(&sj, js, tc.now, jc, sjc, recorder)
  257. expectedCreates := 0
  258. if tc.expectCreate {
  259. expectedCreates = 1
  260. }
  261. if len(jc.Jobs) != expectedCreates {
  262. t.Errorf("%s: expected %d job started, actually %v", name, expectedCreates, len(jc.Jobs))
  263. }
  264. for i := range jc.Jobs {
  265. job := &jc.Jobs[i]
  266. controllerRef := metav1.GetControllerOf(job)
  267. if controllerRef == nil {
  268. t.Errorf("%s: expected job to have ControllerRef: %#v", name, job)
  269. } else {
  270. if got, want := controllerRef.APIVersion, "batch/v1beta1"; got != want {
  271. t.Errorf("%s: controllerRef.APIVersion = %q, want %q", name, got, want)
  272. }
  273. if got, want := controllerRef.Kind, "CronJob"; got != want {
  274. t.Errorf("%s: controllerRef.Kind = %q, want %q", name, got, want)
  275. }
  276. if got, want := controllerRef.Name, sj.Name; got != want {
  277. t.Errorf("%s: controllerRef.Name = %q, want %q", name, got, want)
  278. }
  279. if got, want := controllerRef.UID, sj.UID; got != want {
  280. t.Errorf("%s: controllerRef.UID = %q, want %q", name, got, want)
  281. }
  282. if controllerRef.Controller == nil || *controllerRef.Controller != true {
  283. t.Errorf("%s: controllerRef.Controller is not set to true", name)
  284. }
  285. }
  286. }
  287. expectedDeletes := 0
  288. if tc.expectDelete {
  289. expectedDeletes = 1
  290. }
  291. if len(jc.DeleteJobName) != expectedDeletes {
  292. t.Errorf("%s: expected %d job deleted, actually %v", name, expectedDeletes, len(jc.DeleteJobName))
  293. }
  294. // Status update happens once when ranging through job list, and another one if create jobs.
  295. expectUpdates := 1
  296. expectedEvents := 0
  297. if tc.expectCreate {
  298. expectedEvents++
  299. expectUpdates++
  300. }
  301. if tc.expectDelete {
  302. expectedEvents++
  303. }
  304. expectedEvents += tc.expectedWarnings
  305. if len(recorder.Events) != expectedEvents {
  306. t.Errorf("%s: expected %d event, actually %v", name, expectedEvents, len(recorder.Events))
  307. }
  308. numWarnings := 0
  309. for i := 1; i <= len(recorder.Events); i++ {
  310. e := <-recorder.Events
  311. if strings.HasPrefix(e, v1.EventTypeWarning) {
  312. numWarnings++
  313. }
  314. }
  315. if numWarnings != tc.expectedWarnings {
  316. t.Errorf("%s: expected %d warnings, actually %v", name, tc.expectedWarnings, numWarnings)
  317. }
  318. if tc.expectActive != len(sjc.Updates[expectUpdates-1].Status.Active) {
  319. t.Errorf("%s: expected Active size %d, got %d", name, tc.expectActive, len(sjc.Updates[expectUpdates-1].Status.Active))
  320. }
  321. }
  322. }
  323. type CleanupJobSpec struct {
  324. StartTime string
  325. IsFinished bool
  326. IsSuccessful bool
  327. ExpectDelete bool
  328. IsStillInActiveList bool // only when IsFinished is set
  329. }
  330. func TestCleanupFinishedJobs_DeleteOrNot(t *testing.T) {
  331. limitThree := int32(3)
  332. limitTwo := int32(2)
  333. limitOne := int32(1)
  334. limitZero := int32(0)
  335. // Starting times are assumed to be sorted by increasing start time
  336. // in all the test cases
  337. testCases := map[string]struct {
  338. jobSpecs []CleanupJobSpec
  339. now time.Time
  340. successfulJobsHistoryLimit *int32
  341. failedJobsHistoryLimit *int32
  342. expectActive int
  343. }{
  344. "success. job limit reached": {
  345. []CleanupJobSpec{
  346. {"2016-05-19T04:00:00Z", T, T, T, F},
  347. {"2016-05-19T05:00:00Z", T, T, T, F},
  348. {"2016-05-19T06:00:00Z", T, T, F, F},
  349. {"2016-05-19T07:00:00Z", T, T, F, F},
  350. {"2016-05-19T08:00:00Z", F, F, F, F},
  351. {"2016-05-19T09:00:00Z", T, F, F, F},
  352. }, justBeforeTheHour(), &limitTwo, &limitOne, 1},
  353. "success. jobs not processed by Sync yet": {
  354. []CleanupJobSpec{
  355. {"2016-05-19T04:00:00Z", T, T, T, F},
  356. {"2016-05-19T05:00:00Z", T, T, T, T},
  357. {"2016-05-19T06:00:00Z", T, T, F, T},
  358. {"2016-05-19T07:00:00Z", T, T, F, T},
  359. {"2016-05-19T08:00:00Z", F, F, F, F},
  360. {"2016-05-19T09:00:00Z", T, F, F, T},
  361. }, justBeforeTheHour(), &limitTwo, &limitOne, 4},
  362. "failed job limit reached": {
  363. []CleanupJobSpec{
  364. {"2016-05-19T04:00:00Z", T, F, T, F},
  365. {"2016-05-19T05:00:00Z", T, F, T, F},
  366. {"2016-05-19T06:00:00Z", T, T, F, F},
  367. {"2016-05-19T07:00:00Z", T, T, F, F},
  368. {"2016-05-19T08:00:00Z", T, F, F, F},
  369. {"2016-05-19T09:00:00Z", T, F, F, F},
  370. }, justBeforeTheHour(), &limitTwo, &limitTwo, 0},
  371. "success. job limit set to zero": {
  372. []CleanupJobSpec{
  373. {"2016-05-19T04:00:00Z", T, T, T, F},
  374. {"2016-05-19T05:00:00Z", T, F, T, F},
  375. {"2016-05-19T06:00:00Z", T, T, T, F},
  376. {"2016-05-19T07:00:00Z", T, T, T, F},
  377. {"2016-05-19T08:00:00Z", F, F, F, F},
  378. {"2016-05-19T09:00:00Z", T, F, F, F},
  379. }, justBeforeTheHour(), &limitZero, &limitOne, 1},
  380. "failed job limit set to zero": {
  381. []CleanupJobSpec{
  382. {"2016-05-19T04:00:00Z", T, T, F, F},
  383. {"2016-05-19T05:00:00Z", T, F, T, F},
  384. {"2016-05-19T06:00:00Z", T, T, F, F},
  385. {"2016-05-19T07:00:00Z", T, T, F, F},
  386. {"2016-05-19T08:00:00Z", F, F, F, F},
  387. {"2016-05-19T09:00:00Z", T, F, T, F},
  388. }, justBeforeTheHour(), &limitThree, &limitZero, 1},
  389. "no limits reached": {
  390. []CleanupJobSpec{
  391. {"2016-05-19T04:00:00Z", T, T, F, F},
  392. {"2016-05-19T05:00:00Z", T, F, F, F},
  393. {"2016-05-19T06:00:00Z", T, T, F, F},
  394. {"2016-05-19T07:00:00Z", T, T, F, F},
  395. {"2016-05-19T08:00:00Z", T, F, F, F},
  396. {"2016-05-19T09:00:00Z", T, F, F, F},
  397. }, justBeforeTheHour(), &limitThree, &limitThree, 0},
  398. // This test case should trigger the short-circuit
  399. "limits disabled": {
  400. []CleanupJobSpec{
  401. {"2016-05-19T04:00:00Z", T, T, F, F},
  402. {"2016-05-19T05:00:00Z", T, F, F, F},
  403. {"2016-05-19T06:00:00Z", T, T, F, F},
  404. {"2016-05-19T07:00:00Z", T, T, F, F},
  405. {"2016-05-19T08:00:00Z", T, F, F, F},
  406. {"2016-05-19T09:00:00Z", T, F, F, F},
  407. }, justBeforeTheHour(), nil, nil, 0},
  408. "success limit disabled": {
  409. []CleanupJobSpec{
  410. {"2016-05-19T04:00:00Z", T, T, F, F},
  411. {"2016-05-19T05:00:00Z", T, F, F, F},
  412. {"2016-05-19T06:00:00Z", T, T, F, F},
  413. {"2016-05-19T07:00:00Z", T, T, F, F},
  414. {"2016-05-19T08:00:00Z", T, F, F, F},
  415. {"2016-05-19T09:00:00Z", T, F, F, F},
  416. }, justBeforeTheHour(), nil, &limitThree, 0},
  417. "failure limit disabled": {
  418. []CleanupJobSpec{
  419. {"2016-05-19T04:00:00Z", T, T, F, F},
  420. {"2016-05-19T05:00:00Z", T, F, F, F},
  421. {"2016-05-19T06:00:00Z", T, T, F, F},
  422. {"2016-05-19T07:00:00Z", T, T, F, F},
  423. {"2016-05-19T08:00:00Z", T, F, F, F},
  424. {"2016-05-19T09:00:00Z", T, F, F, F},
  425. }, justBeforeTheHour(), &limitThree, nil, 0},
  426. "no limits reached because still active": {
  427. []CleanupJobSpec{
  428. {"2016-05-19T04:00:00Z", F, F, F, F},
  429. {"2016-05-19T05:00:00Z", F, F, F, F},
  430. {"2016-05-19T06:00:00Z", F, F, F, F},
  431. {"2016-05-19T07:00:00Z", F, F, F, F},
  432. {"2016-05-19T08:00:00Z", F, F, F, F},
  433. {"2016-05-19T09:00:00Z", F, F, F, F},
  434. }, justBeforeTheHour(), &limitZero, &limitZero, 6},
  435. }
  436. for name, tc := range testCases {
  437. sj := cronJob()
  438. suspend := false
  439. sj.Spec.ConcurrencyPolicy = f
  440. sj.Spec.Suspend = &suspend
  441. sj.Spec.Schedule = onTheHour
  442. sj.Spec.SuccessfulJobsHistoryLimit = tc.successfulJobsHistoryLimit
  443. sj.Spec.FailedJobsHistoryLimit = tc.failedJobsHistoryLimit
  444. var (
  445. job *batchv1.Job
  446. err error
  447. )
  448. // Set consistent timestamps for the CronJob
  449. if len(tc.jobSpecs) != 0 {
  450. firstTime := startTimeStringToTime(tc.jobSpecs[0].StartTime)
  451. lastTime := startTimeStringToTime(tc.jobSpecs[len(tc.jobSpecs)-1].StartTime)
  452. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: firstTime}
  453. sj.Status.LastScheduleTime = &metav1.Time{Time: lastTime}
  454. } else {
  455. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeTheHour()}
  456. }
  457. // Create jobs
  458. js := []batchv1.Job{}
  459. jobsToDelete := sets.NewString()
  460. sj.Status.Active = []v1.ObjectReference{}
  461. for i, spec := range tc.jobSpecs {
  462. job, err = getJobFromTemplate(&sj, startTimeStringToTime(spec.StartTime))
  463. if err != nil {
  464. t.Fatalf("%s: unexpected error creating a job from template: %v", name, err)
  465. }
  466. job.UID = types.UID(strconv.Itoa(i))
  467. job.Namespace = ""
  468. if spec.IsFinished {
  469. var conditionType batchv1.JobConditionType
  470. if spec.IsSuccessful {
  471. conditionType = batchv1.JobComplete
  472. } else {
  473. conditionType = batchv1.JobFailed
  474. }
  475. condition := batchv1.JobCondition{Type: conditionType, Status: v1.ConditionTrue}
  476. job.Status.Conditions = append(job.Status.Conditions, condition)
  477. if spec.IsStillInActiveList {
  478. sj.Status.Active = append(sj.Status.Active, v1.ObjectReference{UID: job.UID})
  479. }
  480. } else {
  481. if spec.IsSuccessful || spec.IsStillInActiveList {
  482. t.Errorf("%s: test setup error: this case makes no sense", name)
  483. }
  484. sj.Status.Active = append(sj.Status.Active, v1.ObjectReference{UID: job.UID})
  485. }
  486. js = append(js, *job)
  487. if spec.ExpectDelete {
  488. jobsToDelete.Insert(job.Name)
  489. }
  490. }
  491. jc := &fakeJobControl{Job: job}
  492. sjc := &fakeSJControl{}
  493. recorder := record.NewFakeRecorder(10)
  494. cleanupFinishedJobs(&sj, js, jc, sjc, recorder)
  495. // Check we have actually deleted the correct jobs
  496. if len(jc.DeleteJobName) != len(jobsToDelete) {
  497. t.Errorf("%s: expected %d job deleted, actually %d", name, len(jobsToDelete), len(jc.DeleteJobName))
  498. } else {
  499. jcDeleteJobName := sets.NewString(jc.DeleteJobName...)
  500. if !jcDeleteJobName.Equal(jobsToDelete) {
  501. t.Errorf("%s: expected jobs: %v deleted, actually: %v deleted", name, jobsToDelete, jcDeleteJobName)
  502. }
  503. }
  504. // Check for events
  505. expectedEvents := len(jobsToDelete)
  506. if name == "failed list pod err" {
  507. expectedEvents = len(tc.jobSpecs)
  508. }
  509. if len(recorder.Events) != expectedEvents {
  510. t.Errorf("%s: expected %d event, actually %v", name, expectedEvents, len(recorder.Events))
  511. }
  512. // Check for jobs still in active list
  513. numActive := 0
  514. if len(sjc.Updates) != 0 {
  515. numActive = len(sjc.Updates[len(sjc.Updates)-1].Status.Active)
  516. }
  517. if tc.expectActive != numActive {
  518. t.Errorf("%s: expected Active size %d, got %d", name, tc.expectActive, numActive)
  519. }
  520. }
  521. }
  522. // TODO: simulation where the controller randomly doesn't run, and randomly has errors starting jobs or deleting jobs,
  523. // but over time, all jobs run as expected (assuming Allow and no deadline).
  524. // TestSyncOne_Status tests sj.UpdateStatus in syncOne
  525. func TestSyncOne_Status(t *testing.T) {
  526. finishedJob := newJob("1")
  527. finishedJob.Status.Conditions = append(finishedJob.Status.Conditions, batchv1.JobCondition{Type: batchv1.JobComplete, Status: v1.ConditionTrue})
  528. unexpectedJob := newJob("2")
  529. missingJob := newJob("3")
  530. testCases := map[string]struct {
  531. // sj spec
  532. concurrencyPolicy batchV1beta1.ConcurrencyPolicy
  533. suspend bool
  534. schedule string
  535. deadline int64
  536. // sj status
  537. ranPreviously bool
  538. hasFinishedJob bool
  539. // environment
  540. now time.Time
  541. hasUnexpectedJob bool
  542. hasMissingJob bool
  543. beingDeleted bool
  544. // expectations
  545. expectCreate bool
  546. expectDelete bool
  547. }{
  548. "never ran, not time, A": {A, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, F, F, F},
  549. "never ran, not time, F": {f, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, F, F, F},
  550. "never ran, not time, R": {R, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, F, F, F},
  551. "never ran, is time, A": {A, F, onTheHour, noDead, F, F, justAfterTheHour(), F, F, F, T, F},
  552. "never ran, is time, F": {f, F, onTheHour, noDead, F, F, justAfterTheHour(), F, F, F, T, F},
  553. "never ran, is time, R": {R, F, onTheHour, noDead, F, F, justAfterTheHour(), F, F, F, T, F},
  554. "never ran, is time, deleting": {A, F, onTheHour, noDead, F, F, justAfterTheHour(), F, F, T, F, F},
  555. "never ran, is time, suspended": {A, T, onTheHour, noDead, F, F, justAfterTheHour(), F, F, F, F, F},
  556. "never ran, is time, past deadline": {A, F, onTheHour, shortDead, F, F, justAfterTheHour(), F, F, F, F, F},
  557. "never ran, is time, not past deadline": {A, F, onTheHour, longDead, F, F, justAfterTheHour(), F, F, F, T, F},
  558. "prev ran but done, not time, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F, F, F, F},
  559. "prev ran but done, not time, finished job, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, F, F, F},
  560. "prev ran but done, not time, unexpected job, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), T, F, F, F, F},
  561. "prev ran but done, not time, missing job, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, T, F, F, F},
  562. "prev ran but done, not time, missing job, unexpected job, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), T, T, F, F, F},
  563. "prev ran but done, not time, finished job, unexpected job, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), T, F, F, F, F},
  564. "prev ran but done, not time, finished job, missing job, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, T, F, F, F},
  565. "prev ran but done, not time, finished job, missing job, unexpected job, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), T, T, F, F, F},
  566. "prev ran but done, not time, finished job, F": {f, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, F, F, F},
  567. "prev ran but done, not time, missing job, F": {f, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, T, F, F, F},
  568. "prev ran but done, not time, finished job, missing job, F": {f, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, T, F, F, F},
  569. "prev ran but done, not time, unexpected job, R": {R, F, onTheHour, noDead, T, F, justBeforeTheHour(), T, F, F, F, F},
  570. "prev ran but done, is time, A": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), F, F, F, T, F},
  571. "prev ran but done, is time, finished job, A": {A, F, onTheHour, noDead, T, T, justAfterTheHour(), F, F, F, T, F},
  572. "prev ran but done, is time, unexpected job, A": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F, F, T, F},
  573. "prev ran but done, is time, finished job, unexpected job, A": {A, F, onTheHour, noDead, T, T, justAfterTheHour(), T, F, F, T, F},
  574. "prev ran but done, is time, F": {f, F, onTheHour, noDead, T, F, justAfterTheHour(), F, F, F, T, F},
  575. "prev ran but done, is time, finished job, F": {f, F, onTheHour, noDead, T, T, justAfterTheHour(), F, F, F, T, F},
  576. "prev ran but done, is time, unexpected job, F": {f, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F, F, T, F},
  577. "prev ran but done, is time, finished job, unexpected job, F": {f, F, onTheHour, noDead, T, T, justAfterTheHour(), T, F, F, T, F},
  578. "prev ran but done, is time, R": {R, F, onTheHour, noDead, T, F, justAfterTheHour(), F, F, F, T, F},
  579. "prev ran but done, is time, finished job, R": {R, F, onTheHour, noDead, T, T, justAfterTheHour(), F, F, F, T, F},
  580. "prev ran but done, is time, unexpected job, R": {R, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F, F, T, F},
  581. "prev ran but done, is time, finished job, unexpected job, R": {R, F, onTheHour, noDead, T, T, justAfterTheHour(), T, F, F, T, F},
  582. "prev ran but done, is time, deleting": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), F, F, T, F, F},
  583. "prev ran but done, is time, suspended": {A, T, onTheHour, noDead, T, F, justAfterTheHour(), F, F, F, F, F},
  584. "prev ran but done, is time, finished job, suspended": {A, T, onTheHour, noDead, T, T, justAfterTheHour(), F, F, F, F, F},
  585. "prev ran but done, is time, unexpected job, suspended": {A, T, onTheHour, noDead, T, F, justAfterTheHour(), T, F, F, F, F},
  586. "prev ran but done, is time, finished job, unexpected job, suspended": {A, T, onTheHour, noDead, T, T, justAfterTheHour(), T, F, F, F, F},
  587. "prev ran but done, is time, past deadline": {A, F, onTheHour, shortDead, T, F, justAfterTheHour(), F, F, F, F, F},
  588. "prev ran but done, is time, finished job, past deadline": {A, F, onTheHour, shortDead, T, T, justAfterTheHour(), F, F, F, F, F},
  589. "prev ran but done, is time, unexpected job, past deadline": {A, F, onTheHour, shortDead, T, F, justAfterTheHour(), T, F, F, F, F},
  590. "prev ran but done, is time, finished job, unexpected job, past deadline": {A, F, onTheHour, shortDead, T, T, justAfterTheHour(), T, F, F, F, F},
  591. "prev ran but done, is time, not past deadline": {A, F, onTheHour, longDead, T, F, justAfterTheHour(), F, F, F, T, F},
  592. "prev ran but done, is time, finished job, not past deadline": {A, F, onTheHour, longDead, T, T, justAfterTheHour(), F, F, F, T, F},
  593. "prev ran but done, is time, unexpected job, not past deadline": {A, F, onTheHour, longDead, T, F, justAfterTheHour(), T, F, F, T, F},
  594. "prev ran but done, is time, finished job, unexpected job, not past deadline": {A, F, onTheHour, longDead, T, T, justAfterTheHour(), T, F, F, T, F},
  595. }
  596. for name, tc := range testCases {
  597. // Setup the test
  598. sj := cronJob()
  599. sj.Spec.ConcurrencyPolicy = tc.concurrencyPolicy
  600. sj.Spec.Suspend = &tc.suspend
  601. sj.Spec.Schedule = tc.schedule
  602. if tc.deadline != noDead {
  603. sj.Spec.StartingDeadlineSeconds = &tc.deadline
  604. }
  605. if tc.ranPreviously {
  606. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeThePriorHour()}
  607. sj.Status.LastScheduleTime = &metav1.Time{Time: justAfterThePriorHour()}
  608. } else {
  609. if tc.hasFinishedJob || tc.hasUnexpectedJob || tc.hasMissingJob {
  610. t.Errorf("%s: test setup error: this case makes no sense", name)
  611. }
  612. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: justBeforeTheHour()}
  613. }
  614. jobs := []batchv1.Job{}
  615. if tc.hasFinishedJob {
  616. ref, err := getRef(&finishedJob)
  617. if err != nil {
  618. t.Errorf("%s: test setup error: failed to get job's ref: %v.", name, err)
  619. }
  620. sj.Status.Active = []v1.ObjectReference{*ref}
  621. jobs = append(jobs, finishedJob)
  622. }
  623. if tc.hasUnexpectedJob {
  624. jobs = append(jobs, unexpectedJob)
  625. }
  626. if tc.hasMissingJob {
  627. ref, err := getRef(&missingJob)
  628. if err != nil {
  629. t.Errorf("%s: test setup error: failed to get job's ref: %v.", name, err)
  630. }
  631. sj.Status.Active = append(sj.Status.Active, *ref)
  632. }
  633. if tc.beingDeleted {
  634. timestamp := metav1.NewTime(tc.now)
  635. sj.DeletionTimestamp = &timestamp
  636. }
  637. jc := &fakeJobControl{}
  638. sjc := &fakeSJControl{}
  639. recorder := record.NewFakeRecorder(10)
  640. // Run the code
  641. syncOne(&sj, jobs, tc.now, jc, sjc, recorder)
  642. // Status update happens once when ranging through job list, and another one if create jobs.
  643. expectUpdates := 1
  644. // Events happens when there's unexpected / finished jobs, and upon job creation / deletion.
  645. expectedEvents := 0
  646. if tc.expectCreate {
  647. expectUpdates++
  648. expectedEvents++
  649. }
  650. if tc.expectDelete {
  651. expectedEvents++
  652. }
  653. if tc.hasFinishedJob {
  654. expectedEvents++
  655. }
  656. if tc.hasUnexpectedJob {
  657. expectedEvents++
  658. }
  659. if tc.hasMissingJob {
  660. expectedEvents++
  661. }
  662. if len(recorder.Events) != expectedEvents {
  663. t.Errorf("%s: expected %d event, actually %v: %#v", name, expectedEvents, len(recorder.Events), recorder.Events)
  664. }
  665. if expectUpdates != len(sjc.Updates) {
  666. t.Errorf("%s: expected %d status updates, actually %d", name, expectUpdates, len(sjc.Updates))
  667. }
  668. if tc.hasFinishedJob && inActiveList(sjc.Updates[0], finishedJob.UID) {
  669. t.Errorf("%s: expected finished job removed from active list, actually active list = %#v", name, sjc.Updates[0].Status.Active)
  670. }
  671. if tc.hasUnexpectedJob && inActiveList(sjc.Updates[0], unexpectedJob.UID) {
  672. t.Errorf("%s: expected unexpected job not added to active list, actually active list = %#v", name, sjc.Updates[0].Status.Active)
  673. }
  674. if tc.hasMissingJob && inActiveList(sjc.Updates[0], missingJob.UID) {
  675. t.Errorf("%s: expected missing job to be removed from active list, actually active list = %#v", name, sjc.Updates[0].Status.Active)
  676. }
  677. if tc.expectCreate && !sjc.Updates[1].Status.LastScheduleTime.Time.Equal(topOfTheHour()) {
  678. t.Errorf("%s: expected LastScheduleTime updated to %s, got %s", name, topOfTheHour(), sjc.Updates[1].Status.LastScheduleTime)
  679. }
  680. }
  681. }