utils_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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. "reflect"
  16. "sort"
  17. "strings"
  18. "testing"
  19. "time"
  20. batchv1 "k8s.io/api/batch/v1"
  21. batchv1beta1 "k8s.io/api/batch/v1beta1"
  22. "k8s.io/api/core/v1"
  23. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. utilpointer "k8s.io/utils/pointer"
  26. )
  27. func TestGetJobFromTemplate(t *testing.T) {
  28. // getJobFromTemplate() needs to take the job template and copy the labels and annotations
  29. // and other fields, and add a created-by reference.
  30. var one int64 = 1
  31. var no bool
  32. sj := batchv1beta1.CronJob{
  33. ObjectMeta: metav1.ObjectMeta{
  34. Name: "mycronjob",
  35. Namespace: "snazzycats",
  36. UID: types.UID("1a2b3c"),
  37. SelfLink: "/apis/batch/v1/namespaces/snazzycats/jobs/mycronjob",
  38. },
  39. Spec: batchv1beta1.CronJobSpec{
  40. Schedule: "* * * * ?",
  41. ConcurrencyPolicy: batchv1beta1.AllowConcurrent,
  42. JobTemplate: batchv1beta1.JobTemplateSpec{
  43. ObjectMeta: metav1.ObjectMeta{
  44. Labels: map[string]string{"a": "b"},
  45. Annotations: map[string]string{"x": "y"},
  46. },
  47. Spec: batchv1.JobSpec{
  48. ActiveDeadlineSeconds: &one,
  49. ManualSelector: &no,
  50. Template: v1.PodTemplateSpec{
  51. ObjectMeta: metav1.ObjectMeta{
  52. Labels: map[string]string{
  53. "foo": "bar",
  54. },
  55. },
  56. Spec: v1.PodSpec{
  57. Containers: []v1.Container{
  58. {Image: "foo/bar"},
  59. },
  60. },
  61. },
  62. },
  63. },
  64. },
  65. }
  66. var job *batchv1.Job
  67. job, err := getJobFromTemplate(&sj, time.Time{})
  68. if err != nil {
  69. t.Errorf("Did not expect error: %s", err)
  70. }
  71. if !strings.HasPrefix(job.ObjectMeta.Name, "mycronjob-") {
  72. t.Errorf("Wrong Name")
  73. }
  74. if len(job.ObjectMeta.Labels) != 1 {
  75. t.Errorf("Wrong number of labels")
  76. }
  77. if len(job.ObjectMeta.Annotations) != 1 {
  78. t.Errorf("Wrong number of annotations")
  79. }
  80. }
  81. func TestGetParentUIDFromJob(t *testing.T) {
  82. j := &batchv1.Job{
  83. ObjectMeta: metav1.ObjectMeta{
  84. Name: "foobar",
  85. Namespace: metav1.NamespaceDefault,
  86. },
  87. Spec: batchv1.JobSpec{
  88. Selector: &metav1.LabelSelector{
  89. MatchLabels: map[string]string{"foo": "bar"},
  90. },
  91. Template: v1.PodTemplateSpec{
  92. ObjectMeta: metav1.ObjectMeta{
  93. Labels: map[string]string{
  94. "foo": "bar",
  95. },
  96. },
  97. Spec: v1.PodSpec{
  98. Containers: []v1.Container{
  99. {Image: "foo/bar"},
  100. },
  101. },
  102. },
  103. },
  104. Status: batchv1.JobStatus{
  105. Conditions: []batchv1.JobCondition{{
  106. Type: batchv1.JobComplete,
  107. Status: v1.ConditionTrue,
  108. }},
  109. },
  110. }
  111. {
  112. // Case 1: No ControllerRef
  113. _, found := getParentUIDFromJob(*j)
  114. if found {
  115. t.Errorf("Unexpectedly found uid")
  116. }
  117. }
  118. {
  119. // Case 2: Has ControllerRef
  120. j.ObjectMeta.SetOwnerReferences([]metav1.OwnerReference{
  121. {
  122. Kind: "CronJob",
  123. UID: types.UID("5ef034e0-1890-11e6-8935-42010af0003e"),
  124. Controller: utilpointer.BoolPtr(true),
  125. },
  126. })
  127. expectedUID := types.UID("5ef034e0-1890-11e6-8935-42010af0003e")
  128. uid, found := getParentUIDFromJob(*j)
  129. if !found {
  130. t.Errorf("Unexpectedly did not find uid")
  131. } else if uid != expectedUID {
  132. t.Errorf("Wrong UID: %v", uid)
  133. }
  134. }
  135. }
  136. func TestGroupJobsByParent(t *testing.T) {
  137. uid1 := types.UID("11111111-1111-1111-1111-111111111111")
  138. uid2 := types.UID("22222222-2222-2222-2222-222222222222")
  139. uid3 := types.UID("33333333-3333-3333-3333-333333333333")
  140. ownerReference1 := metav1.OwnerReference{
  141. Kind: "CronJob",
  142. UID: uid1,
  143. Controller: utilpointer.BoolPtr(true),
  144. }
  145. ownerReference2 := metav1.OwnerReference{
  146. Kind: "CronJob",
  147. UID: uid2,
  148. Controller: utilpointer.BoolPtr(true),
  149. }
  150. ownerReference3 := metav1.OwnerReference{
  151. Kind: "CronJob",
  152. UID: uid3,
  153. Controller: utilpointer.BoolPtr(true),
  154. }
  155. {
  156. // Case 1: There are no jobs and scheduledJobs
  157. js := []batchv1.Job{}
  158. jobsBySj := groupJobsByParent(js)
  159. if len(jobsBySj) != 0 {
  160. t.Errorf("Wrong number of items in map")
  161. }
  162. }
  163. {
  164. // Case 2: there is one controller with one job it created.
  165. js := []batchv1.Job{
  166. {ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "x", OwnerReferences: []metav1.OwnerReference{ownerReference1}}},
  167. }
  168. jobsBySj := groupJobsByParent(js)
  169. if len(jobsBySj) != 1 {
  170. t.Errorf("Wrong number of items in map")
  171. }
  172. jobList1, found := jobsBySj[uid1]
  173. if !found {
  174. t.Errorf("Key not found")
  175. }
  176. if len(jobList1) != 1 {
  177. t.Errorf("Wrong number of items in map")
  178. }
  179. }
  180. {
  181. // Case 3: Two namespaces, one has two jobs from one controller, other has 3 jobs from two controllers.
  182. // There are also two jobs with no created-by annotation.
  183. js := []batchv1.Job{
  184. {ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "x", OwnerReferences: []metav1.OwnerReference{ownerReference1}}},
  185. {ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "x", OwnerReferences: []metav1.OwnerReference{ownerReference2}}},
  186. {ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "x", OwnerReferences: []metav1.OwnerReference{ownerReference1}}},
  187. {ObjectMeta: metav1.ObjectMeta{Name: "d", Namespace: "x", OwnerReferences: []metav1.OwnerReference{}}},
  188. {ObjectMeta: metav1.ObjectMeta{Name: "a", Namespace: "y", OwnerReferences: []metav1.OwnerReference{ownerReference3}}},
  189. {ObjectMeta: metav1.ObjectMeta{Name: "b", Namespace: "y", OwnerReferences: []metav1.OwnerReference{ownerReference3}}},
  190. {ObjectMeta: metav1.ObjectMeta{Name: "d", Namespace: "y", OwnerReferences: []metav1.OwnerReference{}}},
  191. }
  192. jobsBySj := groupJobsByParent(js)
  193. if len(jobsBySj) != 3 {
  194. t.Errorf("Wrong number of items in map")
  195. }
  196. jobList1, found := jobsBySj[uid1]
  197. if !found {
  198. t.Errorf("Key not found")
  199. }
  200. if len(jobList1) != 2 {
  201. t.Errorf("Wrong number of items in map")
  202. }
  203. jobList2, found := jobsBySj[uid2]
  204. if !found {
  205. t.Errorf("Key not found")
  206. }
  207. if len(jobList2) != 1 {
  208. t.Errorf("Wrong number of items in map")
  209. }
  210. jobList3, found := jobsBySj[uid3]
  211. if !found {
  212. t.Errorf("Key not found")
  213. }
  214. if len(jobList3) != 2 {
  215. t.Errorf("Wrong number of items in map")
  216. }
  217. }
  218. }
  219. func TestGetRecentUnmetScheduleTimes(t *testing.T) {
  220. // schedule is hourly on the hour
  221. schedule := "0 * * * ?"
  222. // T1 is a scheduled start time of that schedule
  223. T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
  224. if err != nil {
  225. t.Errorf("test setup error: %v", err)
  226. }
  227. // T2 is a scheduled start time of that schedule after T1
  228. T2, err := time.Parse(time.RFC3339, "2016-05-19T11:00:00Z")
  229. if err != nil {
  230. t.Errorf("test setup error: %v", err)
  231. }
  232. sj := batchv1beta1.CronJob{
  233. ObjectMeta: metav1.ObjectMeta{
  234. Name: "mycronjob",
  235. Namespace: metav1.NamespaceDefault,
  236. UID: types.UID("1a2b3c"),
  237. },
  238. Spec: batchv1beta1.CronJobSpec{
  239. Schedule: schedule,
  240. ConcurrencyPolicy: batchv1beta1.AllowConcurrent,
  241. JobTemplate: batchv1beta1.JobTemplateSpec{},
  242. },
  243. }
  244. {
  245. // Case 1: no known start times, and none needed yet.
  246. // Creation time is before T1.
  247. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
  248. // Current time is more than creation time, but less than T1.
  249. now := T1.Add(-7 * time.Minute)
  250. times, err := getRecentUnmetScheduleTimes(sj, now)
  251. if err != nil {
  252. t.Errorf("unexpected error: %v", err)
  253. }
  254. if len(times) != 0 {
  255. t.Errorf("expected no start times, got: %v", times)
  256. }
  257. }
  258. {
  259. // Case 2: no known start times, and one needed.
  260. // Creation time is before T1.
  261. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
  262. // Current time is after T1
  263. now := T1.Add(2 * time.Second)
  264. times, err := getRecentUnmetScheduleTimes(sj, now)
  265. if err != nil {
  266. t.Errorf("unexpected error: %v", err)
  267. }
  268. if len(times) != 1 {
  269. t.Errorf("expected 1 start time, got: %v", times)
  270. } else if !times[0].Equal(T1) {
  271. t.Errorf("expected: %v, got: %v", T1, times[0])
  272. }
  273. }
  274. {
  275. // Case 3: known LastScheduleTime, no start needed.
  276. // Creation time is before T1.
  277. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
  278. // Status shows a start at the expected time.
  279. sj.Status.LastScheduleTime = &metav1.Time{Time: T1}
  280. // Current time is after T1
  281. now := T1.Add(2 * time.Minute)
  282. times, err := getRecentUnmetScheduleTimes(sj, now)
  283. if err != nil {
  284. t.Errorf("unexpected error: %v", err)
  285. }
  286. if len(times) != 0 {
  287. t.Errorf("expected 0 start times, got: %v", times)
  288. }
  289. }
  290. {
  291. // Case 4: known LastScheduleTime, a start needed
  292. // Creation time is before T1.
  293. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
  294. // Status shows a start at the expected time.
  295. sj.Status.LastScheduleTime = &metav1.Time{Time: T1}
  296. // Current time is after T1 and after T2
  297. now := T2.Add(5 * time.Minute)
  298. times, err := getRecentUnmetScheduleTimes(sj, now)
  299. if err != nil {
  300. t.Errorf("unexpected error: %v", err)
  301. }
  302. if len(times) != 1 {
  303. t.Errorf("expected 1 start times, got: %v", times)
  304. } else if !times[0].Equal(T2) {
  305. t.Errorf("expected: %v, got: %v", T1, times[0])
  306. }
  307. }
  308. {
  309. // Case 5: known LastScheduleTime, two starts needed
  310. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)}
  311. sj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)}
  312. // Current time is after T1 and after T2
  313. now := T2.Add(5 * time.Minute)
  314. times, err := getRecentUnmetScheduleTimes(sj, now)
  315. if err != nil {
  316. t.Errorf("unexpected error: %v", err)
  317. }
  318. if len(times) != 2 {
  319. t.Errorf("expected 2 start times, got: %v", times)
  320. } else {
  321. if !times[0].Equal(T1) {
  322. t.Errorf("expected: %v, got: %v", T1, times[0])
  323. }
  324. if !times[1].Equal(T2) {
  325. t.Errorf("expected: %v, got: %v", T2, times[1])
  326. }
  327. }
  328. }
  329. {
  330. // Case 6: now is way way ahead of last start time, and there is no deadline.
  331. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)}
  332. sj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)}
  333. now := T2.Add(10 * 24 * time.Hour)
  334. _, err := getRecentUnmetScheduleTimes(sj, now)
  335. if err == nil {
  336. t.Errorf("expected an error")
  337. }
  338. }
  339. {
  340. // Case 7: now is way way ahead of last start time, but there is a short deadline.
  341. sj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)}
  342. sj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)}
  343. now := T2.Add(10 * 24 * time.Hour)
  344. // Deadline is short
  345. deadline := int64(2 * 60 * 60)
  346. sj.Spec.StartingDeadlineSeconds = &deadline
  347. _, err := getRecentUnmetScheduleTimes(sj, now)
  348. if err != nil {
  349. t.Errorf("unexpected error")
  350. }
  351. }
  352. }
  353. func TestByJobStartTime(t *testing.T) {
  354. now := metav1.NewTime(time.Date(2018, time.January, 1, 2, 3, 4, 5, time.UTC))
  355. later := metav1.NewTime(time.Date(2019, time.January, 1, 2, 3, 4, 5, time.UTC))
  356. aNil := batchv1.Job{
  357. ObjectMeta: metav1.ObjectMeta{Name: "a"},
  358. Status: batchv1.JobStatus{},
  359. }
  360. bNil := batchv1.Job{
  361. ObjectMeta: metav1.ObjectMeta{Name: "b"},
  362. Status: batchv1.JobStatus{},
  363. }
  364. aSet := batchv1.Job{
  365. ObjectMeta: metav1.ObjectMeta{Name: "a"},
  366. Status: batchv1.JobStatus{StartTime: &now},
  367. }
  368. bSet := batchv1.Job{
  369. ObjectMeta: metav1.ObjectMeta{Name: "b"},
  370. Status: batchv1.JobStatus{StartTime: &now},
  371. }
  372. aSetLater := batchv1.Job{
  373. ObjectMeta: metav1.ObjectMeta{Name: "a"},
  374. Status: batchv1.JobStatus{StartTime: &later},
  375. }
  376. testCases := []struct {
  377. name string
  378. input, expected []batchv1.Job
  379. }{
  380. {
  381. name: "both have nil start times",
  382. input: []batchv1.Job{bNil, aNil},
  383. expected: []batchv1.Job{aNil, bNil},
  384. },
  385. {
  386. name: "only the first has a nil start time",
  387. input: []batchv1.Job{aNil, bSet},
  388. expected: []batchv1.Job{bSet, aNil},
  389. },
  390. {
  391. name: "only the second has a nil start time",
  392. input: []batchv1.Job{aSet, bNil},
  393. expected: []batchv1.Job{aSet, bNil},
  394. },
  395. {
  396. name: "both have non-nil, equal start time",
  397. input: []batchv1.Job{bSet, aSet},
  398. expected: []batchv1.Job{aSet, bSet},
  399. },
  400. {
  401. name: "both have non-nil, different start time",
  402. input: []batchv1.Job{aSetLater, bSet},
  403. expected: []batchv1.Job{bSet, aSetLater},
  404. },
  405. }
  406. for _, testCase := range testCases {
  407. sort.Sort(byJobStartTime(testCase.input))
  408. if !reflect.DeepEqual(testCase.input, testCase.expected) {
  409. t.Errorf("case: '%s', jobs not sorted as expected", testCase.name)
  410. }
  411. }
  412. }