1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
7 class ContainerTest < ActiveSupport::TestCase
11 command: ['echo', 'foo'],
12 container_image: 'fa3c1a9cb6783f85f2ecda037e07b8c3+167',
15 runtime_constraints: {"vcpus" => 1, "ram" => 1},
18 REUSABLE_COMMON_ATTRS = {
19 container_image: "9ae44d5792468c58bcf85ce7353c7027+124",
21 command: ["echo", "hello"],
23 runtime_constraints: {
28 "test" => {"kind" => "json"},
35 def minimal_new attrs={}
36 cr = ContainerRequest.new DEFAULT_ATTRS.merge(attrs)
37 cr.state = ContainerRequest::Committed
38 act_as_user users(:active) do
41 c = Container.find_by_uuid cr.container_uuid
46 def check_illegal_updates c, bad_updates
47 bad_updates.each do |u|
48 refute c.update_attributes(u), u.inspect
49 refute c.valid?, u.inspect
54 def check_illegal_modify c
55 check_illegal_updates c, [{command: ["echo", "bar"]},
56 {container_image: "arvados/apitestfixture:june10"},
58 {environment: {"FOO" => "BAR"}},
59 {mounts: {"FOO" => "BAR"}},
60 {output_path: "/tmp3"},
61 {locked_by_uuid: "zzzzz-gj3su-027z32aux8dg2s1"},
62 {auth_uuid: "zzzzz-gj3su-017z32aux8dg2s1"},
63 {runtime_constraints: {"FOO" => "BAR"}}]
66 def check_bogus_states c
67 check_illegal_updates c, [{state: nil},
71 def check_no_change_from_cancelled c
72 check_illegal_modify c
74 check_illegal_updates c, [{ priority: 3 },
75 { state: Container::Queued },
76 { state: Container::Locked },
77 { state: Container::Running },
78 { state: Container::Complete }]
81 test "Container create" do
83 c, _ = minimal_new(environment: {},
84 mounts: {"BAR" => "FOO"},
87 runtime_constraints: {"vcpus" => 1, "ram" => 1})
89 check_illegal_modify c
98 test "Container valid priority" do
100 c, _ = minimal_new(environment: {},
101 mounts: {"BAR" => "FOO"},
104 runtime_constraints: {"vcpus" => 1, "ram" => 1})
106 assert_raises(ActiveRecord::RecordInvalid) do
126 assert_raises(ActiveRecord::RecordInvalid) do
134 test "Container serialized hash attributes sorted before save" do
135 env = {"C" => 3, "B" => 2, "A" => 1}
136 m = {"F" => {"kind" => 3}, "E" => {"kind" => 2}, "D" => {"kind" => 1}}
137 rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1}
138 c, _ = minimal_new(environment: env, mounts: m, runtime_constraints: rc)
139 assert_equal c.environment.to_json, Container.deep_sort_hash(env).to_json
140 assert_equal c.mounts.to_json, Container.deep_sort_hash(m).to_json
141 assert_equal c.runtime_constraints.to_json, Container.deep_sort_hash(rc).to_json
144 test 'deep_sort_hash on array of hashes' do
145 a = {'z' => [[{'a' => 'a', 'b' => 'b'}]]}
146 b = {'z' => [[{'b' => 'b', 'a' => 'a'}]]}
147 assert_equal Container.deep_sort_hash(a).to_json, Container.deep_sort_hash(b).to_json
150 test "find_reusable method should select higher priority queued container" do
151 set_user_from_auth :active
152 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment:{"var" => "queued"}})
153 c_low_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:1}))
154 c_high_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:2}))
155 assert_not_equal c_low_priority.uuid, c_high_priority.uuid
156 assert_equal Container::Queued, c_low_priority.state
157 assert_equal Container::Queued, c_high_priority.state
158 reused = Container.find_reusable(common_attrs)
159 assert_not_nil reused
160 assert_equal reused.uuid, c_high_priority.uuid
163 test "find_reusable method should select latest completed container" do
164 set_user_from_auth :active
165 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "complete"}})
167 state: Container::Complete,
169 log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
170 output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
173 c_older, _ = minimal_new(common_attrs.merge({use_existing: false}))
174 c_recent, _ = minimal_new(common_attrs.merge({use_existing: false}))
175 assert_not_equal c_older.uuid, c_recent.uuid
177 set_user_from_auth :dispatch1
178 c_older.update_attributes!({state: Container::Locked})
179 c_older.update_attributes!({state: Container::Running})
180 c_older.update_attributes!(completed_attrs)
182 c_recent.update_attributes!({state: Container::Locked})
183 c_recent.update_attributes!({state: Container::Running})
184 c_recent.update_attributes!(completed_attrs)
186 reused = Container.find_reusable(common_attrs)
187 assert_not_nil reused
188 assert_equal reused.uuid, c_older.uuid
191 test "find_reusable method should select oldest completed container when inconsistent outputs exist" do
192 set_user_from_auth :active
193 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "complete"}, priority: 1})
195 state: Container::Complete,
197 log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
200 cr = ContainerRequest.new common_attrs
201 cr.use_existing = false
202 cr.state = ContainerRequest::Committed
204 c_output1 = Container.where(uuid: cr.container_uuid).first
206 cr = ContainerRequest.new common_attrs
207 cr.use_existing = false
208 cr.state = ContainerRequest::Committed
210 c_output2 = Container.where(uuid: cr.container_uuid).first
212 assert_not_equal c_output1.uuid, c_output2.uuid
214 set_user_from_auth :dispatch1
216 out1 = '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
217 log1 = collections(:real_log_collection).portable_data_hash
218 c_output1.update_attributes!({state: Container::Locked})
219 c_output1.update_attributes!({state: Container::Running})
220 c_output1.update_attributes!(completed_attrs.merge({log: log1, output: out1}))
222 out2 = 'fa7aeb5140e2848d39b416daeef4ffc5+45'
223 c_output2.update_attributes!({state: Container::Locked})
224 c_output2.update_attributes!({state: Container::Running})
225 c_output2.update_attributes!(completed_attrs.merge({log: log1, output: out2}))
227 reused = Container.resolve(ContainerRequest.new(common_attrs))
228 assert_equal c_output1.uuid, reused.uuid
231 test "find_reusable method should select running container by start date" do
232 set_user_from_auth :active
233 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running"}})
234 c_slower, _ = minimal_new(common_attrs.merge({use_existing: false}))
235 c_faster_started_first, _ = minimal_new(common_attrs.merge({use_existing: false}))
236 c_faster_started_second, _ = minimal_new(common_attrs.merge({use_existing: false}))
237 # Confirm the 3 container UUIDs are different.
238 assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
239 set_user_from_auth :dispatch1
240 c_slower.update_attributes!({state: Container::Locked})
241 c_slower.update_attributes!({state: Container::Running,
243 c_faster_started_first.update_attributes!({state: Container::Locked})
244 c_faster_started_first.update_attributes!({state: Container::Running,
246 c_faster_started_second.update_attributes!({state: Container::Locked})
247 c_faster_started_second.update_attributes!({state: Container::Running,
249 reused = Container.find_reusable(common_attrs)
250 assert_not_nil reused
251 # Selected container is the one that started first
252 assert_equal reused.uuid, c_faster_started_first.uuid
255 test "find_reusable method should select running container by progress" do
256 set_user_from_auth :active
257 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running2"}})
258 c_slower, _ = minimal_new(common_attrs.merge({use_existing: false}))
259 c_faster_started_first, _ = minimal_new(common_attrs.merge({use_existing: false}))
260 c_faster_started_second, _ = minimal_new(common_attrs.merge({use_existing: false}))
261 # Confirm the 3 container UUIDs are different.
262 assert_equal 3, [c_slower.uuid, c_faster_started_first.uuid, c_faster_started_second.uuid].uniq.length
263 set_user_from_auth :dispatch1
264 c_slower.update_attributes!({state: Container::Locked})
265 c_slower.update_attributes!({state: Container::Running,
267 c_faster_started_first.update_attributes!({state: Container::Locked})
268 c_faster_started_first.update_attributes!({state: Container::Running,
270 c_faster_started_second.update_attributes!({state: Container::Locked})
271 c_faster_started_second.update_attributes!({state: Container::Running,
273 reused = Container.find_reusable(common_attrs)
274 assert_not_nil reused
275 # Selected container is the one with most progress done
276 assert_equal reused.uuid, c_faster_started_second.uuid
279 test "find_reusable method should select locked container most likely to start sooner" do
280 set_user_from_auth :active
281 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "locked"}})
282 c_low_priority, _ = minimal_new(common_attrs.merge({use_existing: false}))
283 c_high_priority_older, _ = minimal_new(common_attrs.merge({use_existing: false}))
284 c_high_priority_newer, _ = minimal_new(common_attrs.merge({use_existing: false}))
285 # Confirm the 3 container UUIDs are different.
286 assert_equal 3, [c_low_priority.uuid, c_high_priority_older.uuid, c_high_priority_newer.uuid].uniq.length
287 set_user_from_auth :dispatch1
288 c_low_priority.update_attributes!({state: Container::Locked,
290 c_high_priority_older.update_attributes!({state: Container::Locked,
292 c_high_priority_newer.update_attributes!({state: Container::Locked,
294 reused = Container.find_reusable(common_attrs)
295 assert_not_nil reused
296 assert_equal reused.uuid, c_high_priority_older.uuid
299 test "find_reusable method should select running over failed container" do
300 set_user_from_auth :active
301 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "failed_vs_running"}})
302 c_failed, _ = minimal_new(common_attrs.merge({use_existing: false}))
303 c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
304 assert_not_equal c_failed.uuid, c_running.uuid
305 set_user_from_auth :dispatch1
306 c_failed.update_attributes!({state: Container::Locked})
307 c_failed.update_attributes!({state: Container::Running})
308 c_failed.update_attributes!({state: Container::Complete,
310 log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
311 output: 'ea10d51bcf88862dbcc36eb292017dfd+45'})
312 c_running.update_attributes!({state: Container::Locked})
313 c_running.update_attributes!({state: Container::Running,
315 reused = Container.find_reusable(common_attrs)
316 assert_not_nil reused
317 assert_equal reused.uuid, c_running.uuid
320 test "find_reusable method should select complete over running container" do
321 set_user_from_auth :active
322 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "completed_vs_running"}})
323 c_completed, _ = minimal_new(common_attrs.merge({use_existing: false}))
324 c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
325 assert_not_equal c_completed.uuid, c_running.uuid
326 set_user_from_auth :dispatch1
327 c_completed.update_attributes!({state: Container::Locked})
328 c_completed.update_attributes!({state: Container::Running})
329 c_completed.update_attributes!({state: Container::Complete,
331 log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
332 output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'})
333 c_running.update_attributes!({state: Container::Locked})
334 c_running.update_attributes!({state: Container::Running,
336 reused = Container.find_reusable(common_attrs)
337 assert_not_nil reused
338 assert_equal c_completed.uuid, reused.uuid
341 test "find_reusable method should select running over locked container" do
342 set_user_from_auth :active
343 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running_vs_locked"}})
344 c_locked, _ = minimal_new(common_attrs.merge({use_existing: false}))
345 c_running, _ = minimal_new(common_attrs.merge({use_existing: false}))
346 assert_not_equal c_running.uuid, c_locked.uuid
347 set_user_from_auth :dispatch1
348 c_locked.update_attributes!({state: Container::Locked})
349 c_running.update_attributes!({state: Container::Locked})
350 c_running.update_attributes!({state: Container::Running,
352 reused = Container.find_reusable(common_attrs)
353 assert_not_nil reused
354 assert_equal reused.uuid, c_running.uuid
357 test "find_reusable method should select locked over queued container" do
358 set_user_from_auth :active
359 common_attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "running_vs_locked"}})
360 c_locked, _ = minimal_new(common_attrs.merge({use_existing: false}))
361 c_queued, _ = minimal_new(common_attrs.merge({use_existing: false}))
362 assert_not_equal c_queued.uuid, c_locked.uuid
363 set_user_from_auth :dispatch1
364 c_locked.update_attributes!({state: Container::Locked})
365 reused = Container.find_reusable(common_attrs)
366 assert_not_nil reused
367 assert_equal reused.uuid, c_locked.uuid
370 test "find_reusable method should not select failed container" do
371 set_user_from_auth :active
372 attrs = REUSABLE_COMMON_ATTRS.merge({environment: {"var" => "failed"}})
373 c, _ = minimal_new(attrs)
374 set_user_from_auth :dispatch1
375 c.update_attributes!({state: Container::Locked})
376 c.update_attributes!({state: Container::Running})
377 c.update_attributes!({state: Container::Complete,
379 reused = Container.find_reusable(attrs)
383 test "find_reusable with logging disabled" do
384 set_user_from_auth :active
385 Rails.logger.expects(:info).never
386 Container.find_reusable(REUSABLE_COMMON_ATTRS)
389 test "find_reusable with logging enabled" do
390 set_user_from_auth :active
391 Rails.configuration.log_reuse_decisions = true
392 Rails.logger.expects(:info).at_least(3)
393 Container.find_reusable(REUSABLE_COMMON_ATTRS)
396 test "Container running" do
397 c, _ = minimal_new priority: 1
399 set_user_from_auth :dispatch1
400 check_illegal_updates c, [{state: Container::Running},
401 {state: Container::Complete}]
404 c.update_attributes! state: Container::Running
406 check_illegal_modify c
409 check_illegal_updates c, [{state: Container::Queued}]
412 c.update_attributes! priority: 3
415 test "Lock and unlock" do
416 c, cr = minimal_new priority: 0
418 set_user_from_auth :dispatch1
419 assert_equal Container::Queued, c.state
421 assert_raise(ArvadosModel::LockFailedError) do
426 assert cr.update_attributes priority: 1
428 refute c.update_attributes(state: Container::Running), "not locked"
430 refute c.update_attributes(state: Container::Complete), "not locked"
433 assert c.lock, show_errors(c)
434 assert c.locked_by_uuid
437 assert_raise(ArvadosModel::LockFailedError) {c.lock}
440 assert c.unlock, show_errors(c)
441 refute c.locked_by_uuid
444 refute c.update_attributes(state: Container::Running), "not locked"
446 refute c.locked_by_uuid
449 assert c.lock, show_errors(c)
450 assert c.update_attributes(state: Container::Running), show_errors(c)
451 assert c.locked_by_uuid
454 auth_uuid_was = c.auth_uuid
456 assert_raise(ArvadosModel::LockFailedError) do
457 # Running to Locked is not allowed
461 assert_raise(ArvadosModel::InvalidStateTransitionError) do
462 # Running to Queued is not allowed
467 assert c.update_attributes(state: Container::Complete), show_errors(c)
468 refute c.locked_by_uuid
471 auth_exp = ApiClientAuthorization.find_by_uuid(auth_uuid_was).expires_at
472 assert_operator auth_exp, :<, db_current_time
475 test "Container queued cancel" do
477 set_user_from_auth :dispatch1
478 assert c.update_attributes(state: Container::Cancelled), show_errors(c)
479 check_no_change_from_cancelled c
482 test "Container queued count" do
483 assert_equal 1, Container.readable_by(users(:active)).where(state: "Queued").count
486 test "Container locked cancel" do
488 set_user_from_auth :dispatch1
489 assert c.lock, show_errors(c)
490 assert c.update_attributes(state: Container::Cancelled), show_errors(c)
491 check_no_change_from_cancelled c
494 test "Container locked cancel with log" do
496 set_user_from_auth :dispatch1
497 assert c.lock, show_errors(c)
498 assert c.update_attributes(
499 state: Container::Cancelled,
500 log: collections(:real_log_collection).portable_data_hash,
502 check_no_change_from_cancelled c
505 test "Container running cancel" do
507 set_user_from_auth :dispatch1
509 c.update_attributes! state: Container::Running
510 c.update_attributes! state: Container::Cancelled
511 check_no_change_from_cancelled c
514 test "Container create forbidden for non-admin" do
515 set_user_from_auth :active_trustedclient
516 c = Container.new DEFAULT_ATTRS
518 c.mounts = {"BAR" => "FOO"}
519 c.output_path = "/tmp"
521 c.runtime_constraints = {}
522 assert_raises(ArvadosModel::PermissionDeniedError) do
527 test "Container only set exit code on complete" do
529 set_user_from_auth :dispatch1
531 c.update_attributes! state: Container::Running
533 check_illegal_updates c, [{exit_code: 1},
534 {exit_code: 1, state: Container::Cancelled}]
536 assert c.update_attributes(exit_code: 1, state: Container::Complete)
539 test "locked_by_uuid can set output on running container" do
541 set_user_from_auth :dispatch1
543 c.update_attributes! state: Container::Running
545 assert_equal c.locked_by_uuid, Thread.current[:api_client_authorization].uuid
547 assert c.update_attributes output: collections(:collection_owned_by_active).portable_data_hash
548 assert c.update_attributes! state: Container::Complete
551 test "auth_uuid can set output on running container, but not change container state" do
553 set_user_from_auth :dispatch1
555 c.update_attributes! state: Container::Running
557 Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
558 Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
559 assert c.update_attributes output: collections(:collection_owned_by_active).portable_data_hash
561 assert_raises ArvadosModel::PermissionDeniedError do
562 # auth_uuid cannot set container state
563 c.update_attributes state: Container::Complete
567 test "not allowed to set output that is not readable by current user" do
569 set_user_from_auth :dispatch1
571 c.update_attributes! state: Container::Running
573 Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
574 Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
576 assert_raises ActiveRecord::RecordInvalid do
577 c.update_attributes! output: collections(:collection_not_readable_by_active).portable_data_hash
581 test "other token cannot set output on running container" do
583 set_user_from_auth :dispatch1
585 c.update_attributes! state: Container::Running
587 set_user_from_auth :running_to_be_deleted_container_auth
588 assert_raises ArvadosModel::PermissionDeniedError do
589 c.update_attributes! output: collections(:foo_file).portable_data_hash
593 test "can set trashed output on running container" do
595 set_user_from_auth :dispatch1
597 c.update_attributes! state: Container::Running
599 output = Collection.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jk')
601 assert output.is_trashed
602 assert c.update_attributes output: output.portable_data_hash
603 assert c.update_attributes! state: Container::Complete
606 test "not allowed to set trashed output that is not readable by current user" do
608 set_user_from_auth :dispatch1
610 c.update_attributes! state: Container::Running
612 output = Collection.find_by_uuid('zzzzz-4zz18-mto52zx1s7sn3jr')
614 Thread.current[:api_client_authorization] = ApiClientAuthorization.find_by_uuid(c.auth_uuid)
615 Thread.current[:user] = User.find_by_id(Thread.current[:api_client_authorization].user_id)
617 assert_raises ActiveRecord::RecordInvalid do
618 c.update_attributes! output: output.portable_data_hash