controller_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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 scheduledjob
  14. import (
  15. "testing"
  16. "time"
  17. "k8s.io/kubernetes/pkg/api"
  18. "k8s.io/kubernetes/pkg/api/unversioned"
  19. "k8s.io/kubernetes/pkg/apis/batch"
  20. "k8s.io/kubernetes/pkg/client/record"
  21. "k8s.io/kubernetes/pkg/types"
  22. )
  23. // schedule is hourly on the hour
  24. var (
  25. onTheHour string = "0 * * * ?"
  26. )
  27. func justBeforeTheHour() time.Time {
  28. T1, err := time.Parse(time.RFC3339, "2016-05-19T09:59:00Z")
  29. if err != nil {
  30. panic("test setup error")
  31. }
  32. return T1
  33. }
  34. func topOfTheHour() time.Time {
  35. T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
  36. if err != nil {
  37. panic("test setup error")
  38. }
  39. return T1
  40. }
  41. func justAfterTheHour() time.Time {
  42. T1, err := time.Parse(time.RFC3339, "2016-05-19T10:01:00Z")
  43. if err != nil {
  44. panic("test setup error")
  45. }
  46. return T1
  47. }
  48. func justBeforeThePriorHour() time.Time {
  49. T1, err := time.Parse(time.RFC3339, "2016-05-19T08:59:00Z")
  50. if err != nil {
  51. panic("test setup error")
  52. }
  53. return T1
  54. }
  55. func justAfterThePriorHour() time.Time {
  56. T1, err := time.Parse(time.RFC3339, "2016-05-19T09:01:00Z")
  57. if err != nil {
  58. panic("test setup error")
  59. }
  60. return T1
  61. }
  62. // returns a scheduledJob with some fields filled in.
  63. func scheduledJob() batch.ScheduledJob {
  64. return batch.ScheduledJob{
  65. ObjectMeta: api.ObjectMeta{
  66. Name: "myscheduledjob",
  67. Namespace: "snazzycats",
  68. UID: types.UID("1a2b3c"),
  69. SelfLink: "/apis/batch/v2alpha1/namespaces/snazzycats/jobs/myscheduledjob",
  70. CreationTimestamp: unversioned.Time{Time: justBeforeTheHour()},
  71. },
  72. Spec: batch.ScheduledJobSpec{
  73. Schedule: "* * * * ?",
  74. ConcurrencyPolicy: batch.AllowConcurrent,
  75. JobTemplate: batch.JobTemplateSpec{
  76. ObjectMeta: api.ObjectMeta{
  77. Labels: map[string]string{"a": "b"},
  78. Annotations: map[string]string{"x": "y"},
  79. },
  80. Spec: jobSpec(),
  81. },
  82. },
  83. }
  84. }
  85. func jobSpec() batch.JobSpec {
  86. one := int32(1)
  87. return batch.JobSpec{
  88. Parallelism: &one,
  89. Completions: &one,
  90. Template: api.PodTemplateSpec{
  91. ObjectMeta: api.ObjectMeta{
  92. Labels: map[string]string{
  93. "foo": "bar",
  94. },
  95. },
  96. Spec: api.PodSpec{
  97. Containers: []api.Container{
  98. {Image: "foo/bar"},
  99. },
  100. },
  101. },
  102. }
  103. }
  104. func newJob(UID string) batch.Job {
  105. return batch.Job{
  106. ObjectMeta: api.ObjectMeta{
  107. UID: types.UID(UID),
  108. Name: "foobar",
  109. Namespace: api.NamespaceDefault,
  110. SelfLink: "/apis/batch/v1/namespaces/snazzycats/jobs/myjob",
  111. },
  112. Spec: jobSpec(),
  113. }
  114. }
  115. var (
  116. shortDead int64 = 10
  117. longDead int64 = 1000000
  118. noDead int64 = -12345
  119. A batch.ConcurrencyPolicy = batch.AllowConcurrent
  120. f batch.ConcurrencyPolicy = batch.ForbidConcurrent
  121. R batch.ConcurrencyPolicy = batch.ReplaceConcurrent
  122. T bool = true
  123. F bool = false
  124. )
  125. func TestSyncOne_RunOrNot(t *testing.T) {
  126. testCases := map[string]struct {
  127. // sj spec
  128. concurrencyPolicy batch.ConcurrencyPolicy
  129. suspend bool
  130. schedule string
  131. deadline int64
  132. // sj status
  133. ranPreviously bool
  134. stillActive bool
  135. // environment
  136. now time.Time
  137. // expectations
  138. expectCreate bool
  139. expectDelete bool
  140. }{
  141. "never ran, not time, A": {A, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F},
  142. "never ran, not time, F": {f, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F},
  143. "never ran, not time, R": {R, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F},
  144. "never ran, is time, A": {A, F, onTheHour, noDead, F, F, justAfterTheHour(), T, F},
  145. "never ran, is time, F": {f, F, onTheHour, noDead, F, F, justAfterTheHour(), T, F},
  146. "never ran, is time, R": {R, F, onTheHour, noDead, F, F, justAfterTheHour(), T, F},
  147. "never ran, is time, suspended": {A, T, onTheHour, noDead, F, F, justAfterTheHour(), F, F},
  148. "never ran, is time, past deadline": {A, F, onTheHour, shortDead, F, F, justAfterTheHour(), F, F},
  149. "never ran, is time, not past deadline": {A, F, onTheHour, longDead, F, F, justAfterTheHour(), T, F},
  150. "prev ran but done, not time, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F},
  151. "prev ran but done, not time, F": {f, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F},
  152. "prev ran but done, not time, R": {R, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F},
  153. "prev ran but done, is time, A": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F},
  154. "prev ran but done, is time, F": {f, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F},
  155. "prev ran but done, is time, R": {R, F, onTheHour, noDead, T, F, justAfterTheHour(), T, F},
  156. "prev ran but done, is time, suspended": {A, T, onTheHour, noDead, T, F, justAfterTheHour(), F, F},
  157. "prev ran but done, is time, past deadline": {A, F, onTheHour, shortDead, T, F, justAfterTheHour(), F, F},
  158. "prev ran but done, is time, not past deadline": {A, F, onTheHour, longDead, T, F, justAfterTheHour(), T, F},
  159. "still active, not time, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F},
  160. "still active, not time, F": {f, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F},
  161. "still active, not time, R": {R, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F},
  162. "still active, is time, A": {A, F, onTheHour, noDead, T, T, justAfterTheHour(), T, F},
  163. "still active, is time, F": {f, F, onTheHour, noDead, T, T, justAfterTheHour(), F, F},
  164. "still active, is time, R": {R, F, onTheHour, noDead, T, T, justAfterTheHour(), T, T},
  165. "still active, is time, suspended": {A, T, onTheHour, noDead, T, T, justAfterTheHour(), F, F},
  166. "still active, is time, past deadline": {A, F, onTheHour, shortDead, T, T, justAfterTheHour(), F, F},
  167. "still active, is time, not past deadline": {A, F, onTheHour, longDead, T, T, justAfterTheHour(), T, F},
  168. }
  169. for name, tc := range testCases {
  170. t.Log("Test case:", name)
  171. sj := scheduledJob()
  172. sj.Spec.ConcurrencyPolicy = tc.concurrencyPolicy
  173. sj.Spec.Suspend = &tc.suspend
  174. sj.Spec.Schedule = tc.schedule
  175. if tc.deadline != noDead {
  176. sj.Spec.StartingDeadlineSeconds = &tc.deadline
  177. }
  178. var (
  179. job *batch.Job
  180. err error
  181. )
  182. js := []batch.Job{}
  183. if tc.ranPreviously {
  184. sj.ObjectMeta.CreationTimestamp = unversioned.Time{Time: justBeforeThePriorHour()}
  185. sj.Status.LastScheduleTime = &unversioned.Time{Time: justAfterThePriorHour()}
  186. job, err = getJobFromTemplate(&sj, sj.Status.LastScheduleTime.Time)
  187. if err != nil {
  188. t.Fatalf("Unexpected error creating a job from template: %v", err)
  189. }
  190. job.UID = "1234"
  191. job.Namespace = ""
  192. if tc.stillActive {
  193. sj.Status.Active = []api.ObjectReference{{UID: job.UID}}
  194. js = append(js, *job)
  195. }
  196. } else {
  197. sj.ObjectMeta.CreationTimestamp = unversioned.Time{Time: justBeforeTheHour()}
  198. if tc.stillActive {
  199. t.Errorf("Test setup error: this case makes no sense.")
  200. }
  201. }
  202. jc := &fakeJobControl{Job: job}
  203. sjc := &fakeSJControl{}
  204. pc := &fakePodControl{}
  205. recorder := record.NewFakeRecorder(10)
  206. SyncOne(sj, js, tc.now, jc, sjc, pc, recorder)
  207. expectedCreates := 0
  208. if tc.expectCreate {
  209. expectedCreates = 1
  210. }
  211. if len(jc.Jobs) != expectedCreates {
  212. t.Errorf("Expected %d job started, actually %v", expectedCreates, len(jc.Jobs))
  213. }
  214. expectedDeletes := 0
  215. if tc.expectDelete {
  216. expectedDeletes = 1
  217. }
  218. if len(jc.DeleteJobName) != expectedDeletes {
  219. t.Errorf("Expected %d job deleted, actually %v", expectedDeletes, len(jc.DeleteJobName))
  220. }
  221. expectedEvents := 0
  222. if tc.expectCreate {
  223. expectedEvents += 1
  224. }
  225. if tc.expectDelete {
  226. expectedEvents += 1
  227. }
  228. if len(recorder.Events) != expectedEvents {
  229. t.Errorf("Expected %d event, actually %v", expectedEvents, len(recorder.Events))
  230. }
  231. }
  232. }
  233. // TODO: simulation where the controller randomly doesn't run, and randomly has errors starting jobs or deleting jobs,
  234. // but over time, all jobs run as expected (assuming Allow and no deadline).
  235. // TestSyncOne_Status tests sj.UpdateStatus in SyncOne
  236. func TestSyncOne_Status(t *testing.T) {
  237. finishedJob := newJob("1")
  238. finishedJob.Status.Conditions = append(finishedJob.Status.Conditions, batch.JobCondition{Type: batch.JobComplete, Status: api.ConditionTrue})
  239. unexpectedJob := newJob("2")
  240. testCases := map[string]struct {
  241. // sj spec
  242. concurrencyPolicy batch.ConcurrencyPolicy
  243. suspend bool
  244. schedule string
  245. deadline int64
  246. // sj status
  247. ranPreviously bool
  248. hasFinishedJob bool
  249. // environment
  250. now time.Time
  251. hasUnexpectedJob bool
  252. // expectations
  253. expectCreate bool
  254. expectDelete bool
  255. }{
  256. "never ran, not time, A": {A, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, F},
  257. "never ran, not time, F": {f, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, F},
  258. "never ran, not time, R": {R, F, onTheHour, noDead, F, F, justBeforeTheHour(), F, F, F},
  259. "never ran, is time, A": {A, F, onTheHour, noDead, F, F, justAfterTheHour(), F, T, F},
  260. "never ran, is time, F": {f, F, onTheHour, noDead, F, F, justAfterTheHour(), F, T, F},
  261. "never ran, is time, R": {R, F, onTheHour, noDead, F, F, justAfterTheHour(), F, T, F},
  262. "never ran, is time, suspended": {A, T, onTheHour, noDead, F, F, justAfterTheHour(), F, F, F},
  263. "never ran, is time, past deadline": {A, F, onTheHour, shortDead, F, F, justAfterTheHour(), F, F, F},
  264. "never ran, is time, not past deadline": {A, F, onTheHour, longDead, F, F, justAfterTheHour(), F, T, F},
  265. "prev ran but done, not time, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), F, F, F},
  266. "prev ran but done, not time, finished job, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, F},
  267. "prev ran but done, not time, unexpected job, A": {A, F, onTheHour, noDead, T, F, justBeforeTheHour(), T, F, F},
  268. "prev ran but done, not time, finished job, unexpected job, A": {A, F, onTheHour, noDead, T, T, justBeforeTheHour(), T, F, F},
  269. "prev ran but done, not time, finished job, F": {f, F, onTheHour, noDead, T, T, justBeforeTheHour(), F, F, F},
  270. "prev ran but done, not time, unexpected job, R": {R, F, onTheHour, noDead, T, F, justBeforeTheHour(), T, F, F},
  271. "prev ran but done, is time, A": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), F, T, F},
  272. "prev ran but done, is time, finished job, A": {A, F, onTheHour, noDead, T, T, justAfterTheHour(), F, T, F},
  273. "prev ran but done, is time, unexpected job, A": {A, F, onTheHour, noDead, T, F, justAfterTheHour(), T, T, F},
  274. "prev ran but done, is time, finished job, unexpected job, A": {A, F, onTheHour, noDead, T, T, justAfterTheHour(), T, T, F},
  275. "prev ran but done, is time, F": {f, F, onTheHour, noDead, T, F, justAfterTheHour(), F, T, F},
  276. "prev ran but done, is time, finished job, F": {f, F, onTheHour, noDead, T, T, justAfterTheHour(), F, T, F},
  277. "prev ran but done, is time, unexpected job, F": {f, F, onTheHour, noDead, T, F, justAfterTheHour(), T, T, F},
  278. "prev ran but done, is time, finished job, unexpected job, F": {f, F, onTheHour, noDead, T, T, justAfterTheHour(), T, T, F},
  279. "prev ran but done, is time, R": {R, F, onTheHour, noDead, T, F, justAfterTheHour(), F, T, F},
  280. "prev ran but done, is time, finished job, R": {R, F, onTheHour, noDead, T, T, justAfterTheHour(), F, T, F},
  281. "prev ran but done, is time, unexpected job, R": {R, F, onTheHour, noDead, T, F, justAfterTheHour(), T, T, F},
  282. "prev ran but done, is time, finished job, unexpected job, R": {R, F, onTheHour, noDead, T, T, justAfterTheHour(), T, T, F},
  283. "prev ran but done, is time, suspended": {A, T, onTheHour, noDead, T, F, justAfterTheHour(), F, F, F},
  284. "prev ran but done, is time, finished job, suspended": {A, T, onTheHour, noDead, T, T, justAfterTheHour(), F, F, F},
  285. "prev ran but done, is time, unexpected job, suspended": {A, T, onTheHour, noDead, T, F, justAfterTheHour(), T, F, F},
  286. "prev ran but done, is time, finished job, unexpected job, suspended": {A, T, onTheHour, noDead, T, T, justAfterTheHour(), T, F, F},
  287. "prev ran but done, is time, past deadline": {A, F, onTheHour, shortDead, T, F, justAfterTheHour(), F, F, F},
  288. "prev ran but done, is time, finished job, past deadline": {A, F, onTheHour, shortDead, T, T, justAfterTheHour(), F, F, F},
  289. "prev ran but done, is time, unexpected job, past deadline": {A, F, onTheHour, shortDead, T, F, justAfterTheHour(), T, F, F},
  290. "prev ran but done, is time, finished job, unexpected job, past deadline": {A, F, onTheHour, shortDead, T, T, justAfterTheHour(), T, F, F},
  291. "prev ran but done, is time, not past deadline": {A, F, onTheHour, longDead, T, F, justAfterTheHour(), F, T, F},
  292. "prev ran but done, is time, finished job, not past deadline": {A, F, onTheHour, longDead, T, T, justAfterTheHour(), F, T, F},
  293. "prev ran but done, is time, unexpected job, not past deadline": {A, F, onTheHour, longDead, T, F, justAfterTheHour(), T, T, F},
  294. "prev ran but done, is time, finished job, unexpected job, not past deadline": {A, F, onTheHour, longDead, T, T, justAfterTheHour(), T, T, F},
  295. }
  296. for name, tc := range testCases {
  297. t.Log("Test case:", name)
  298. // Setup the test
  299. sj := scheduledJob()
  300. sj.Spec.ConcurrencyPolicy = tc.concurrencyPolicy
  301. sj.Spec.Suspend = &tc.suspend
  302. sj.Spec.Schedule = tc.schedule
  303. if tc.deadline != noDead {
  304. sj.Spec.StartingDeadlineSeconds = &tc.deadline
  305. }
  306. if tc.ranPreviously {
  307. sj.ObjectMeta.CreationTimestamp = unversioned.Time{Time: justBeforeThePriorHour()}
  308. sj.Status.LastScheduleTime = &unversioned.Time{Time: justAfterThePriorHour()}
  309. } else {
  310. if tc.hasFinishedJob || tc.hasUnexpectedJob {
  311. t.Errorf("Test setup error: this case makes no sense.")
  312. }
  313. sj.ObjectMeta.CreationTimestamp = unversioned.Time{Time: justBeforeTheHour()}
  314. }
  315. jobs := []batch.Job{}
  316. if tc.hasFinishedJob {
  317. ref, err := getRef(&finishedJob)
  318. if err != nil {
  319. t.Errorf("Test setup error: failed to get job's ref: %v.", err)
  320. }
  321. sj.Status.Active = []api.ObjectReference{*ref}
  322. jobs = append(jobs, finishedJob)
  323. }
  324. if tc.hasUnexpectedJob {
  325. jobs = append(jobs, unexpectedJob)
  326. }
  327. jc := &fakeJobControl{}
  328. sjc := &fakeSJControl{}
  329. pc := &fakePodControl{}
  330. recorder := record.NewFakeRecorder(10)
  331. // Run the code
  332. SyncOne(sj, jobs, tc.now, jc, sjc, pc, recorder)
  333. // Status update happens once when ranging through job list, and another one if create jobs.
  334. expectUpdates := 1
  335. // Events happens when there's unexpected / finished jobs, and upon job creation / deletion.
  336. expectedEvents := 0
  337. if tc.expectCreate {
  338. expectUpdates++
  339. expectedEvents++
  340. }
  341. if tc.expectDelete {
  342. expectedEvents++
  343. }
  344. if tc.hasFinishedJob {
  345. expectedEvents++
  346. }
  347. if tc.hasUnexpectedJob {
  348. expectedEvents++
  349. }
  350. if len(recorder.Events) != expectedEvents {
  351. t.Errorf("Expected %d event, actually %v: %#v", expectedEvents, len(recorder.Events), recorder.Events)
  352. }
  353. if expectUpdates != len(sjc.Updates) {
  354. t.Errorf("expected %d status updates, actually %d", expectUpdates, len(sjc.Updates))
  355. }
  356. if tc.hasFinishedJob && inActiveList(sjc.Updates[0], finishedJob.UID) {
  357. t.Errorf("Expected finished job removed from active list, actually active list = %#v.", sjc.Updates[0].Status.Active)
  358. }
  359. if tc.hasUnexpectedJob && inActiveList(sjc.Updates[0], unexpectedJob.UID) {
  360. t.Errorf("Expected unexpected job not added to active list, actually active list = %#v.", sjc.Updates[0].Status.Active)
  361. }
  362. if tc.expectCreate && !sjc.Updates[1].Status.LastScheduleTime.Time.Equal(topOfTheHour()) {
  363. t.Errorf("Expected LastScheduleTime updated to %s, got %s.", topOfTheHour(), sjc.Updates[1].Status.LastScheduleTime)
  364. }
  365. }
  366. }