end
end
end
+
+ def secret_mounts
+ if @object &&
+ @object.auth_uuid &&
+ @object.auth_uuid == Thread.current[:api_client_authorization].uuid
+ send_json({"secret_mounts" => @object.secret_mounts})
+ else
+ send_error("Token is not associated with this container.", status: 403)
+ end
+ end
end
serialize :runtime_constraints, Hash
serialize :command, Array
serialize :scheduling_parameters, Hash
+ serialize :secret_mounts, Hash
before_validation :fill_field_defaults, :if => :new_record?
before_validation :set_timestamps
validate :validate_output
after_validation :assign_auth
before_save :sort_serialized_attrs
+ before_save :update_secret_mounts_md5
+ before_save :scrub_secret_mounts
after_save :handle_completed
after_save :propagate_priority
["mounts"]
end
+ def self.full_text_searchable_columns
+ super - ["secret_mounts", "secret_mounts_md5"]
+ end
+
+ def self.searchable_columns *args
+ super - ["secret_mounts_md5"]
+ end
+
+ def logged_attributes
+ super.except('secret_mounts')
+ end
+
def state_transitions
State_transitions
end
mounts: resolve_mounts(req.mounts),
runtime_constraints: resolve_runtime_constraints(req.runtime_constraints),
scheduling_parameters: req.scheduling_parameters,
+ secret_mounts: req.secret_mounts,
}
act_as_system_user do
if req.use_existing && (reusable = find_reusable(c_attrs))
candidates = candidates.where_serialized(:mounts, resolve_mounts(attrs[:mounts]))
log_reuse_info(candidates) { "after filtering on mounts #{attrs[:mounts].inspect}" }
+ candidates = candidates.where('secret_mounts_md5 = ?', Digest::MD5.hexdigest(SafeJSON.dump(self.deep_sort_hash(attrs[:secret_mounts] || {}))))
+ log_reuse_info(candidates) { "after filtering on mounts #{attrs[:mounts].inspect}" }
+
candidates = candidates.where_serialized(:runtime_constraints, resolve_runtime_constraints(attrs[:runtime_constraints]))
log_reuse_info(candidates) { "after filtering on runtime_constraints #{attrs[:runtime_constraints].inspect}" }
if self.new_record?
permitted.push(:owner_uuid, :command, :container_image, :cwd,
:environment, :mounts, :output_path, :priority,
- :runtime_constraints, :scheduling_parameters)
+ :runtime_constraints, :scheduling_parameters,
+ :secret_mounts)
end
case self.state
end
end
+ def update_secret_mounts_md5
+ if self.secret_mounts_changed?
+ self.secret_mounts_md5 = Digest::MD5.hexdigest(
+ SafeJSON.dump(self.class.deep_sort_hash(self.secret_mounts)))
+ end
+ end
+
+ def scrub_secret_mounts
+ # this runs after update_secret_mounts_md5, so the
+ # secret_mounts_md5 will still reflect the secrets that are being
+ # scrubbed here.
+ if self.state_changed? && self.final?
+ self.secret_mounts = {}
+ end
+ end
+
def handle_completed
# This container is finished so finalize any associated container requests
# that are associated with this container.
serialize :runtime_constraints, Hash
serialize :command, Array
serialize :scheduling_parameters, Hash
+ serialize :secret_mounts, Hash
before_validation :fill_field_defaults, :if => :new_record?
before_validation :validate_runtime_constraints
validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 1000 }
validate :validate_state_change
validate :check_update_whitelist
+ before_save :scrub_secret_mounts
after_save :update_priority
after_save :finalize_if_needed
before_create :set_requesting_container_uuid
:container_image, :cwd, :environment, :filters, :mounts,
:output_path, :priority, :properties, :requesting_container_uuid,
:runtime_constraints, :state, :container_uuid, :use_existing,
- :scheduling_parameters, :output_name, :output_ttl]
+ :scheduling_parameters, :secret_mounts, :output_name, :output_ttl]
def self.limit_index_columns_read
["mounts"]
end
+ def logged_attributes
+ super.except('secret_mounts')
+ end
+
def state_transitions
State_transitions
end
end
def self.full_text_searchable_columns
- super - ["mounts"]
+ super - ["mounts", "secret_mounts", "secret_mounts_md5"]
end
protected
if self.new_record? || self.state_was == Uncommitted
# Allow create-and-commit in a single operation.
- permitted.push *AttrsPermittedBeforeCommit
+ permitted.push(*AttrsPermittedBeforeCommit)
end
case self.state
super(permitted)
end
+ def scrub_secret_mounts
+ if self.state == Final
+ self.secret_mounts = {}
+ end
+ end
+
def update_priority
if self.state_changed? or
self.priority_changed? or
}
exceptions = %w(controller action format id)
params = event.payload[:params].except(*exceptions)
+
+ # Omit secret_mounts field if supplied in create/update request
+ # body.
+ [
+ ['container', 'secret_mounts'],
+ ['container_request', 'secret_mounts'],
+ ].each do |resource, field|
+ if params[resource].is_a? Hash
+ params[resource] = params[resource].except(field)
+ end
+ end
+
params_s = SafeJSON.dump(params)
if params_s.length > Rails.configuration.max_request_log_params_size
payload[:params_truncated] = params_s[0..Rails.configuration.max_request_log_params_size] + "[...]"
get 'auth', on: :member
post 'lock', on: :member
post 'unlock', on: :member
+ get 'secret_mounts', on: :member
get 'current', on: :collection
end
resources :container_requests
--- /dev/null
+class AddSecretMountsToContainers < ActiveRecord::Migration
+ def change
+ add_column :container_requests, :secret_mounts, :jsonb, default: {}
+ add_column :containers, :secret_mounts, :jsonb, default: {}
+ add_column :containers, :secret_mounts_md5, :string, default: "99914b932bd37a50b983c5e7c90ae93b"
+ add_index :containers, :secret_mounts_md5
+ end
+end
output_uuid character varying(255),
log_uuid character varying(255),
output_name character varying(255) DEFAULT NULL::character varying,
- output_ttl integer DEFAULT 0 NOT NULL
+ output_ttl integer DEFAULT 0 NOT NULL,
+ secret_mounts jsonb DEFAULT '{}'::jsonb
);
exit_code integer,
auth_uuid character varying(255),
locked_by_uuid character varying(255),
- scheduling_parameters text
+ scheduling_parameters text,
+ secret_mounts jsonb DEFAULT '{}'::jsonb,
+ secret_mounts_md5 character varying DEFAULT '99914b932bd37a50b983c5e7c90ae93b'::character varying
);
CREATE INDEX index_containers_on_owner_uuid ON containers USING btree (owner_uuid);
+--
+-- Name: index_containers_on_secret_mounts_md5; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_containers_on_secret_mounts_md5 ON containers USING btree (secret_mounts_md5);
+
+
--
-- Name: index_containers_on_uuid; Type: INDEX; Schema: public; Owner: -
--
INSERT INTO schema_migrations (version) VALUES ('20180216203422');
+INSERT INTO schema_migrations (version) VALUES ('20180228220311');
+
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts:
+ /secret/6x9:
+ kind: text
+ content: "42\n"
+ secret_mounts_md5: <%= Digest::MD5.hexdigest(SafeJSON.dump({'/secret/6x9' => {'kind' => 'text', 'content' => "42\n"}})) %>
auth_uuid: zzzzz-gj3su-077z32aux8dg2s2
running_older:
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
locked:
uuid: zzzzz-dz642-lockedcontainer
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
completed:
uuid: zzzzz-dz642-compltcontainer
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
completed_older:
uuid: zzzzz-dz642-compltcontainr2
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
requester:
uuid: zzzzz-dz642-requestingcntnr
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
requester_container:
uuid: zzzzz-dz642-requestercntnr1
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
failed_container:
uuid: zzzzz-dz642-failedcontainr1
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
ancient_container_with_logs:
uuid: zzzzz-dz642-logscontainer01
finished_at: <%= 2.year.ago.to_s(:db) %>
log: ea10d51bcf88862dbcc36eb292017dfd+45
output: test
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
previous_container_with_logs:
uuid: zzzzz-dz642-logscontainer02
finished_at: <%= 1.month.ago.to_s(:db) %>
log: ea10d51bcf88862dbcc36eb292017dfd+45
output: test
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
running_container_with_logs:
uuid: zzzzz-dz642-logscontainer03
runtime_constraints:
ram: 12000000000
vcpus: 4
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
running_to_be_deleted:
uuid: zzzzz-dz642-runnincntrtodel
ram: 12000000000
vcpus: 4
auth_uuid: zzzzz-gj3su-ty6lvu9d7u7c2sq
+ secret_mounts: {}
+ secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
require 'test_helper'
class Arvados::V1::ContainerRequestsControllerTest < ActionController::TestCase
+ def minimal_cr
+ {
+ command: ['echo', 'hello'],
+ container_image: 'test',
+ output_path: 'test',
+ }
+ end
+
test 'create with scheduling parameters' do
- authorize_with :system_user
+ authorize_with :active
sp = {'partitions' => ['test1', 'test2']}
post :create, {
- container_request: {
- command: ['echo', 'hello'],
- container_image: 'test',
- output_path: 'test',
- scheduling_parameters: sp,
- },
- }
+ container_request: minimal_cr.merge(scheduling_parameters: sp.dup)
+ }
assert_response :success
cr = JSON.parse(@response.body)
assert_not_nil cr, 'Expected container request'
assert_equal sp, cr['scheduling_parameters']
end
+
+ test "secret_mounts not in #create responses" do
+ authorize_with :active
+
+ post :create, {
+ container_request: minimal_cr.merge(
+ secret_mounts: {'/foo' => {'type' => 'json', 'content' => 'bar'}}),
+ }
+ assert_response :success
+
+ resp = JSON.parse(@response.body)
+ refute resp.has_key?('secret_mounts')
+
+ req = ContainerRequest.where(uuid: resp['uuid']).first
+ assert_equal 'bar', req.secret_mounts['/foo']['content']
+ end
+
+ test "update with secret_mounts" do
+ authorize_with :active
+ req = container_requests(:uncommitted)
+
+ patch :update, {
+ id: req.uuid,
+ container_request: {
+ secret_mounts: {'/foo' => {'type' => 'json', 'content' => 'bar'}},
+ },
+ }
+ assert_response :success
+
+ resp = JSON.parse(@response.body)
+ refute resp.has_key?('secret_mounts')
+
+ req.reload
+ assert_equal 'bar', req.secret_mounts['/foo']['content']
+ end
+
+ test "update without deleting secret_mounts" do
+ authorize_with :active
+ req = container_requests(:uncommitted)
+ req.update_attributes!(secret_mounts: {'/foo' => {'type' => 'json', 'content' => 'bar'}})
+
+ patch :update, {
+ id: req.uuid,
+ container_request: {
+ command: ['echo', 'test'],
+ },
+ }
+ assert_response :success
+
+ resp = JSON.parse(@response.body)
+ refute resp.has_key?('secret_mounts')
+
+ req.reload
+ assert_equal 'bar', req.secret_mounts['/foo']['content']
+ end
end
assert_equal 'arvados#apiClientAuthorization', json_response['kind']
end
- test 'no auth in container response' do
+ test 'no auth or secret_mounts in container response' do
authorize_with :dispatch1
c = containers(:queued)
assert c.lock, show_errors(c)
get :show, id: c.uuid
assert_response :success
assert_nil json_response['auth']
+ assert_nil json_response['secret_mounts']
end
test "lock container" do
[:running, :lock, 422, 'Running'],
[:running, :unlock, 422, 'Running'],
].each do |fixture, action, response, state|
- test "state transitions from #{fixture } to #{action}" do
+ test "state transitions from #{fixture} to #{action}" do
authorize_with :dispatch1
uuid = containers(fixture).uuid
post action, {id: uuid}
assert_response 401
end
+ [
+ [true, :running_container_auth],
+ [false, :dispatch2],
+ [false, :admin],
+ [false, :active],
+ ].each do |expect_success, auth|
+ test "get secret_mounts with #{auth} token" do
+ authorize_with auth
+ get :secret_mounts, {id: containers(:running).uuid}
+ if expect_success
+ assert_response :success
+ assert_equal "42\n", json_response["secret_mounts"]["/secret/6x9"]["content"]
+ else
+ assert_response 403
+ end
+ end
+ end
end
--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+module ContainerTestHelper
+ def secret_string
+ 'UNGU3554BL3'
+ end
+
+ def assert_no_secrets_logged
+ Log.all.map(&:properties).each do |props|
+ refute_match /secret\/6x9|#{secret_string}/, SafeJSON.dump(props)
+ end
+ end
+end
# SPDX-License-Identifier: AGPL-3.0
require 'test_helper'
+require 'helpers/container_test_helper'
require 'helpers/docker_migration_helper'
class ContainerRequestTest < ActiveSupport::TestCase
include DockerMigrationHelper
include DbCurrentTime
+ include ContainerTestHelper
def create_minimal_req! attrs={}
defaults = {
end
end
+ test "reuse container with same secret_mounts" do
+ set_user_from_auth :active
+ cr1 = create_minimal_req!(state: "Committed", priority: 1)
+ cr1.save!
+ cr2 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: {})
+ cr2.save!
+ assert_not_nil cr1.container_uuid
+ assert_equal cr1.container_uuid, cr2.container_uuid
+ end
+
+ secrets = {"/foo" => {"kind" => "binary"}}
+ [
+ [true, nil, nil],
+ [true, nil, {}],
+ [true, {}, {}],
+ [true, secrets, secrets],
+ [false, nil, secrets],
+ [false, {}, secrets],
+ ].each do |expect_reuse, sm1, sm2|
+ test "container reuse secret_mounts #{sm1.inspect}, #{sm2.inspect}" do
+ set_user_from_auth :active
+ cr1 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: sm1)
+ cr2 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: sm2)
+ assert_not_nil cr1.container_uuid
+ assert_not_nil cr2.container_uuid
+ if expect_reuse
+ assert_equal cr1.container_uuid, cr2.container_uuid
+ else
+ assert_not_equal cr1.container_uuid, cr2.container_uuid
+ end
+ end
+ end
+
+ test "scrub secret_mounts but reuse container for request with identical secret_mounts" do
+ set_user_from_auth :active
+ sm = {'/secret/foo' => {'kind' => 'text', 'content' => secret_string}}
+ cr1 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: sm.dup)
+ run_container(cr1)
+ cr1.reload
+
+ # secret_mounts scrubbed from db
+ c = Container.where(uuid: cr1.container_uuid).first
+ assert_equal({}, c.secret_mounts)
+ assert_equal({}, cr1.secret_mounts)
+
+ # can reuse container if secret_mounts match
+ cr2 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: sm.dup)
+ assert_equal cr1.container_uuid, cr2.container_uuid
+
+ # don't reuse container if secret_mounts don't match
+ cr3 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: {})
+ assert_not_equal cr1.container_uuid, cr3.container_uuid
+
+ assert_no_secrets_logged
+ end
+
+ test "not reuse container with different secret_mounts" do
+ secrets = {"/foo" => {"kind" => "binary"}}
+ set_user_from_auth :active
+ cr1 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: secrets.dup)
+ cr1.save!
+ cr2 = create_minimal_req!(state: "Committed", priority: 1, secret_mounts: secrets.dup)
+ cr2.save!
+ assert_not_nil cr1.container_uuid
+ assert_equal cr1.container_uuid, cr2.container_uuid
+ end
end
# SPDX-License-Identifier: AGPL-3.0
require 'test_helper'
+require 'helpers/container_test_helper'
class ContainerTest < ActiveSupport::TestCase
include DbCurrentTime
+ include ContainerTestHelper
DEFAULT_ATTRS = {
command: ['echo', 'foo'],
end
end
+ [
+ Container::Complete,
+ Container::Cancelled,
+ ].each do |final_state|
+ test "secret_mounts is null after container is #{final_state}" do
+ assert_no_secrets_logged
+ end
+ end
end