2879: API server can find_or_create Jobs based on Docker image.
[arvados.git] / services / api / test / functional / arvados / v1 / job_reuse_controller_test.rb
1 require 'test_helper'
2 load 'test/functional/arvados/v1/git_setup.rb'
3
4 class Arvados::V1::JobReuseControllerTest < ActionController::TestCase
5   fixtures :repositories, :users, :jobs, :links, :collections
6
7   # See git_setup.rb for the commit log for test.git.tar
8   include GitSetup
9
10   setup do
11     @controller = Arvados::V1::JobsController.new
12     authorize_with :active
13   end
14
15   test "reuse job with no_reuse=false" do
16     post :create, job: {
17       no_reuse: false,
18       script: "hash",
19       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
20       repository: "foo",
21       script_parameters: {
22         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
23         an_integer: '1'
24       }
25     }
26     assert_response :success
27     assert_not_nil assigns(:object)
28     new_job = JSON.parse(@response.body)
29     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
30     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
31   end
32
33   test "reuse job with find_or_create=true" do
34     post :create, {
35       job: {
36         script: "hash",
37         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
38         repository: "foo",
39         script_parameters: {
40           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
41           an_integer: '1'
42         }
43       },
44       find_or_create: true
45     }
46     assert_response :success
47     assert_not_nil assigns(:object)
48     new_job = JSON.parse(@response.body)
49     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
50     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
51   end
52
53   test "do not reuse job because no_reuse=true" do
54     post :create, {
55       job: {
56         no_reuse: true,
57         script: "hash",
58         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
59         repository: "foo",
60         script_parameters: {
61           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
62           an_integer: '1'
63         }
64       }
65     }
66     assert_response :success
67     assert_not_nil assigns(:object)
68     new_job = JSON.parse(@response.body)
69     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
70     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
71   end
72
73   test "do not reuse job because find_or_create=false" do
74     post :create, {
75       job: {
76         script: "hash",
77         script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
78         repository: "foo",
79         script_parameters: {
80           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
81           an_integer: '1'
82         }
83       },
84       find_or_create: false
85     }
86     assert_response :success
87     assert_not_nil assigns(:object)
88     new_job = JSON.parse(@response.body)
89     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
90     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
91   end
92
93   test "test_cannot_reuse_job_no_output" do
94     post :create, job: {
95       no_reuse: false,
96       script: "hash",
97       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
98       repository: "foo",
99       script_parameters: {
100         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
101         an_integer: '2'
102       }
103     }
104     assert_response :success
105     assert_not_nil assigns(:object)
106     new_job = JSON.parse(@response.body)
107     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykppp', new_job['uuid']
108   end
109
110   test "test_reuse_job_range" do
111     post :create, job: {
112       no_reuse: false,
113       script: "hash",
114       minimum_script_version: "tag1",
115       script_version: "master",
116       repository: "foo",
117       script_parameters: {
118         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
119         an_integer: '1'
120       }
121     }
122     assert_response :success
123     assert_not_nil assigns(:object)
124     new_job = JSON.parse(@response.body)
125     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
126     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
127   end
128
129   test "cannot_reuse_job_no_minimum_given_so_must_use_specified_commit" do
130     post :create, job: {
131       no_reuse: false,
132       script: "hash",
133       script_version: "master",
134       repository: "foo",
135       script_parameters: {
136         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
137         an_integer: '1'
138       }
139     }
140     assert_response :success
141     assert_not_nil assigns(:object)
142     new_job = JSON.parse(@response.body)
143     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
144     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
145   end
146
147   test "test_cannot_reuse_job_different_input" do
148     post :create, job: {
149       no_reuse: false,
150       script: "hash",
151       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
152       repository: "foo",
153       script_parameters: {
154         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
155         an_integer: '2'
156       }
157     }
158     assert_response :success
159     assert_not_nil assigns(:object)
160     new_job = JSON.parse(@response.body)
161     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
162     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
163   end
164
165   test "test_cannot_reuse_job_different_version" do
166     post :create, job: {
167       no_reuse: false,
168       script: "hash",
169       script_version: "master",
170       repository: "foo",
171       script_parameters: {
172         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
173         an_integer: '2'
174       }
175     }
176     assert_response :success
177     assert_not_nil assigns(:object)
178     new_job = JSON.parse(@response.body)
179     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
180     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
181   end
182
183   test "test_can_reuse_job_submitted_nondeterministic" do
184     post :create, job: {
185       no_reuse: false,
186       script: "hash",
187       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
188       repository: "foo",
189       script_parameters: {
190         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
191         an_integer: '1'
192       },
193       nondeterministic: true
194     }
195     assert_response :success
196     assert_not_nil assigns(:object)
197     new_job = JSON.parse(@response.body)
198     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
199     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
200   end
201
202   test "test_cannot_reuse_job_past_nondeterministic" do
203     post :create, job: {
204       no_reuse: false,
205       script: "hash2",
206       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
207       repository: "foo",
208       script_parameters: {
209         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
210         an_integer: '1'
211       }
212     }
213     assert_response :success
214     assert_not_nil assigns(:object)
215     new_job = JSON.parse(@response.body)
216     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykyyy', new_job['uuid']
217     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
218   end
219
220   test "test_cannot_reuse_job_no_permission" do
221     authorize_with :spectator
222     post :create, job: {
223       no_reuse: false,
224       script: "hash",
225       script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
226       repository: "foo",
227       script_parameters: {
228         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
229         an_integer: '1'
230       }
231     }
232     assert_response :success
233     assert_not_nil assigns(:object)
234     new_job = JSON.parse(@response.body)
235     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
236     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
237   end
238
239   test "test_cannot_reuse_job_excluded" do
240     post :create, job: {
241       no_reuse: false,
242       script: "hash",
243       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
244       script_version: "master",
245       repository: "foo",
246       exclude_script_versions: ["tag1"],
247       script_parameters: {
248         input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
249         an_integer: '1'
250       }
251     }
252     assert_response :success
253     assert_not_nil assigns(:object)
254     new_job = JSON.parse(@response.body)
255     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
256     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
257   end
258
259   test "cannot reuse job with find_or_create but excluded version" do
260     post :create, {
261       job: {
262         script: "hash",
263         script_version: "master",
264         repository: "foo",
265         script_parameters: {
266           input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
267           an_integer: '1'
268         }
269       },
270       find_or_create: true,
271       minimum_script_version: "31ce37fe365b3dc204300a3e4c396ad333ed0556",
272       exclude_script_versions: ["tag1"],
273     }
274     assert_response :success
275     assert_not_nil assigns(:object)
276     new_job = JSON.parse(@response.body)
277     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
278     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
279   end
280
281   BASE_FILTERS = {
282     'repository' => ['=', 'foo'],
283     'script' => ['=', 'hash'],
284     'script_version' => ['in git', 'master'],
285     'docker_image_locator' => ['=', nil],
286   }
287
288   def filters_from_hash(hash)
289     hash.each_pair.map { |name, filter| [name] + filter }
290   end
291
292   test "can reuse a Job based on filters" do
293     filter_h = BASE_FILTERS.
294       merge('script_version' => ['in git', 'tag1'])
295     post(:create, {
296            job: {
297              script: "hash",
298              script_version: "master",
299              repository: "foo",
300              script_parameters: {
301                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
302                an_integer: '1'
303              }
304            },
305            filters: filters_from_hash(filter_h),
306            find_or_create: true,
307          })
308     assert_response :success
309     assert_not_nil assigns(:object)
310     new_job = JSON.parse(@response.body)
311     assert_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
312     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
313   end
314
315   test "can not reuse a Job based on filters" do
316     filter_a = filters_from_hash(BASE_FILTERS.reject { |k| k == 'script_version' })
317     filter_a += [["script_version", "in git",
318                   "31ce37fe365b3dc204300a3e4c396ad333ed0556"],
319                  ["script_version", "not in git", ["tag1"]]]
320     post(:create, {
321            job: {
322              script: "hash",
323              script_version: "master",
324              repository: "foo",
325              script_parameters: {
326                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
327                an_integer: '1'
328              }
329            },
330            filters: filter_a,
331            find_or_create: true,
332          })
333     assert_response :success
334     assert_not_nil assigns(:object)
335     new_job = JSON.parse(@response.body)
336     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
337     assert_equal '077ba2ad3ea24a929091a9e6ce545c93199b8e57', new_job['script_version']
338   end
339
340   test "can not reuse a Job based on arbitrary filters" do
341     filter_h = BASE_FILTERS.
342       merge("created_at" => ["<", "2010-01-01T00:00:00Z"])
343     post(:create, {
344            job: {
345              script: "hash",
346              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
347              repository: "foo",
348              script_parameters: {
349                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
350                an_integer: '1'
351              }
352            },
353            filters: filters_from_hash(filter_h),
354            find_or_create: true,
355          })
356     assert_response :success
357     assert_not_nil assigns(:object)
358     new_job = JSON.parse(@response.body)
359     assert_not_equal 'zzzzz-8i9sb-cjs4pklxxjykqqq', new_job['uuid']
360     assert_equal '4fe459abe02d9b365932b8f5dc419439ab4e2577', new_job['script_version']
361   end
362
363   test "can reuse a Job with a Docker image" do
364     post(:create, {
365            job: {
366              script: "hash",
367              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
368              repository: "foo",
369              script_parameters: {
370                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
371                an_integer: '1'
372              },
373              runtime_constraints: {
374                docker_image: 'arvados/apitestfixture',
375              }
376            },
377            find_or_create: true,
378          })
379     assert_response :success
380     new_job = assigns(:object)
381     assert_not_nil new_job
382     target_job = jobs(:previous_docker_job_run)
383     [:uuid, :script_version, :docker_image_locator].each do |attr|
384       assert_equal(target_job.send(attr), new_job.send(attr))
385     end
386   end
387
388   test "can reuse a Job with a Docker image hash filter" do
389     filter_h = BASE_FILTERS.
390       merge("script_version" =>
391               ["=", "4fe459abe02d9b365932b8f5dc419439ab4e2577"],
392             "docker_image_locator" =>
393               ["in docker", links(:docker_image_collection_hash).name])
394     post(:create, {
395            job: {
396              script: "hash",
397              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
398              repository: "foo",
399              script_parameters: {
400                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
401                an_integer: '1'
402              },
403            },
404            filters: filters_from_hash(filter_h),
405            find_or_create: true,
406          })
407     assert_response :success
408     new_job = assigns(:object)
409     assert_not_nil new_job
410     target_job = jobs(:previous_docker_job_run)
411     [:uuid, :script_version, :docker_image_locator].each do |attr|
412       assert_equal(target_job.send(attr), new_job.send(attr))
413     end
414   end
415
416   test "new job with unknown Docker image filter" do
417     filter_h = BASE_FILTERS.
418       merge("docker_image_locator" => ["in docker", "_nonesuchname_"])
419     post(:create, {
420            job: {
421              script: "hash",
422              script_version: "4fe459abe02d9b365932b8f5dc419439ab4e2577",
423              repository: "foo",
424              script_parameters: {
425                input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
426                an_integer: '1'
427              },
428            },
429            filters: filters_from_hash(filter_h),
430            find_or_create: true,
431          })
432     assert_response :success
433     new_job = assigns(:object)
434     assert_not_nil new_job
435     assert_not_equal(jobs(:previous_docker_job_run).uuid, new_job.uuid)
436   end
437
438   ["repository", "script"].each do |skip_key|
439     test "missing #{skip_key} filter raises an error" do
440       filter_a = filters_from_hash(BASE_FILTERS.reject { |k| k == skip_key })
441       post(:create, {
442              job: {
443                script: "hash",
444                script_version: "master",
445                repository: "foo",
446                script_parameters: {
447                  input: 'fa7aeb5140e2848d39b416daeef4ffc5+45',
448                  an_integer: '1'
449                }
450              },
451              filters: filter_a,
452              find_or_create: true,
453            })
454       assert_includes(405..599, @response.code.to_i,
455                       "bad status code with missing #{skip_key} filter")
456     end
457   end
458 end