X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/18b51f2620505d4efbc9de322e24d5218a5ca19b..33021029867be4a2240f0d3673045dfac7598350:/services/api/app/models/container.rb diff --git a/services/api/app/models/container.rb b/services/api/app/models/container.rb index 5fea2d5bc7..fb900a993d 100644 --- a/services/api/app/models/container.rb +++ b/services/api/app/models/container.rb @@ -17,13 +17,17 @@ class Container < ArvadosModel extend DbCurrentTime extend LogReuseInfo + # Posgresql JSONB columns should NOT be declared as serialized, Rails 5 + # already know how to properly treat them. + attribute :secret_mounts, :jsonbHash, default: {} + attribute :runtime_status, :jsonbHash, default: {} + attribute :runtime_auth_scopes, :jsonbHash, default: {} + serialize :environment, Hash serialize :mounts, Hash serialize :runtime_constraints, Hash serialize :command, Array serialize :scheduling_parameters, Hash - serialize :secret_mounts, Hash - serialize :runtime_status, Hash before_validation :fill_field_defaults, :if => :new_record? before_validation :set_timestamps @@ -37,8 +41,9 @@ class Container < ArvadosModel after_validation :assign_auth before_save :sort_serialized_attrs before_save :update_secret_mounts_md5 - before_save :scrub_secret_mounts + before_save :scrub_secrets before_save :clear_runtime_status_when_queued + after_save :update_cr_logs after_save :handle_completed after_save :propagate_priority after_commit { UpdatePriority.run_update_thread } @@ -66,6 +71,8 @@ class Container < ArvadosModel t.add :state t.add :auth_uuid t.add :scheduling_parameters + t.add :runtime_user_uuid + t.add :runtime_auth_scopes end # Supported states for a container @@ -90,15 +97,15 @@ class Container < ArvadosModel end def self.full_text_searchable_columns - super - ["secret_mounts", "secret_mounts_md5"] + super - ["secret_mounts", "secret_mounts_md5", "runtime_token"] end def self.searchable_columns *args - super - ["secret_mounts_md5"] + super - ["secret_mounts_md5", "runtime_token"] end def logged_attributes - super.except('secret_mounts') + super.except('secret_mounts', 'runtime_token') end def state_transitions @@ -145,17 +152,37 @@ class Container < ArvadosModel # Create a new container (or find an existing one) to satisfy the # given container request. def self.resolve(req) - c_attrs = { - command: req.command, - cwd: req.cwd, - environment: req.environment, - output_path: req.output_path, - container_image: resolve_container_image(req.container_image), - mounts: resolve_mounts(req.mounts), - runtime_constraints: resolve_runtime_constraints(req.runtime_constraints), - scheduling_parameters: req.scheduling_parameters, - secret_mounts: req.secret_mounts, - } + if req.runtime_token.nil? + runtime_user = if req.modified_by_user_uuid.nil? + current_user + else + User.find_by_uuid(req.modified_by_user_uuid) + end + runtime_auth_scopes = ["all"] + else + auth = ApiClientAuthorization.validate(token: req.runtime_token) + if auth.nil? + raise ArgumentError.new "Invalid runtime token" + end + runtime_user = User.find_by_id(auth.user_id) + runtime_auth_scopes = auth.scopes + end + c_attrs = act_as_user runtime_user do + { + command: req.command, + cwd: req.cwd, + environment: req.environment, + output_path: req.output_path, + container_image: resolve_container_image(req.container_image), + mounts: resolve_mounts(req.mounts), + runtime_constraints: resolve_runtime_constraints(req.runtime_constraints), + scheduling_parameters: req.scheduling_parameters, + secret_mounts: req.secret_mounts, + runtime_token: req.runtime_token, + runtime_user_uuid: runtime_user.uuid, + runtime_auth_scopes: runtime_auth_scopes + } + end act_as_system_user do if req.use_existing && (reusable = find_reusable(c_attrs)) reusable @@ -200,7 +227,11 @@ class Container < ArvadosModel if mount['kind'] != 'collection' next end - if (uuid = mount.delete 'uuid') + + uuid = mount.delete 'uuid' + + if mount['portable_data_hash'].nil? and !uuid.nil? + # PDH not supplied, try by UUID c = Collection. readable_by(current_user). where(uuid: uuid). @@ -209,13 +240,7 @@ class Container < ArvadosModel if !c raise ArvadosModel::UnresolvableContainerError.new "cannot mount collection #{uuid.inspect}: not found" end - if mount['portable_data_hash'].nil? - # PDH not supplied by client - mount['portable_data_hash'] = c.portable_data_hash - elsif mount['portable_data_hash'] != c.portable_data_hash - # UUID and PDH supplied by client, but they don't agree - raise ArgumentError.new "cannot mount collection #{uuid.inspect}: current portable_data_hash #{c.portable_data_hash.inspect} does not match #{c['portable_data_hash'].inspect} in request" - end + mount['portable_data_hash'] = c.portable_data_hash end end return c_mounts @@ -325,7 +350,7 @@ class Container < ArvadosModel transaction do reload check_lock_fail - update_attributes!(state: Locked) + update_attributes!(state: Locked, lock_count: self.lock_count+1) end end @@ -343,7 +368,14 @@ class Container < ArvadosModel transaction do reload(lock: 'FOR UPDATE') check_unlock_fail - update_attributes!(state: Queued) + if self.lock_count < Rails.configuration.max_container_dispatch_attempts + update_attributes!(state: Queued) + else + update_attributes!(state: Cancelled, + runtime_status: { + error: "Container exceeded 'max_container_dispatch_attempts' (lock_count=#{self.lock_count}." + }) + end end end @@ -354,6 +386,9 @@ class Container < ArvadosModel else kwargs = {} end + if users_list.select { |u| u.is_admin }.any? + return super + end Container.where(ContainerRequest.readable_by(*users_list).where("containers.uuid = container_requests.container_uuid").exists) end @@ -361,6 +396,19 @@ class Container < ArvadosModel [Complete, Cancelled].include?(self.state) end + def self.for_current_token + return if !current_api_client_authorization + _, _, _, container_uuid = Thread.current[:token].split('/') + if container_uuid.nil? + Container.where(auth_uuid: current_api_client_authorization.uuid).first + else + Container.where('auth_uuid=? or (uuid=? and runtime_token=?)', + current_api_client_authorization.uuid, + container_uuid, + current_api_client_authorization.token).first + end + end + protected def fill_field_defaults @@ -377,24 +425,11 @@ class Container < ArvadosModel current_user.andand.is_admin end - def permission_to_update - # Override base permission check to allow auth_uuid to set progress and - # output (only). Whether it is legal to set progress and output in the current - # state has already been checked in validate_change. - current_user.andand.is_admin || - (!current_api_client_authorization.nil? and - [self.auth_uuid, self.locked_by_uuid].include? current_api_client_authorization.uuid) - end - def ensure_owner_uuid_is_permitted - # Override base permission check to allow auth_uuid to set progress and - # output (only). Whether it is legal to set progress and output in the current - # state has already been checked in validate_change. - if !current_api_client_authorization.nil? and self.auth_uuid == current_api_client_authorization.uuid - check_update_whitelist [:progress, :output] - else - super - end + # validate_change ensures owner_uuid can't be changed at all -- + # except during create, which requires admin privileges. Checking + # permission here would be superfluous. + true end def set_timestamps @@ -420,38 +455,41 @@ class Container < ArvadosModel def validate_change permitted = [:state] + progress_attrs = [:progress, :runtime_status, :log, :output] + final_attrs = [:exit_code, :finished_at] if self.new_record? permitted.push(:owner_uuid, :command, :container_image, :cwd, :environment, :mounts, :output_path, :priority, :runtime_constraints, :scheduling_parameters, - :secret_mounts) + :secret_mounts, :runtime_token, + :runtime_user_uuid, :runtime_auth_scopes) end case self.state when Locked - permitted.push :priority, :runtime_status + permitted.push :priority, :runtime_status, :log, :lock_count when Queued permitted.push :priority when Running - permitted.push :priority, :progress, :output, :runtime_status + permitted.push :priority, *progress_attrs if self.state_changed? permitted.push :started_at end when Complete if self.state_was == Running - permitted.push :finished_at, :output, :log, :exit_code + permitted.push *final_attrs, *progress_attrs end when Cancelled case self.state_was when Running - permitted.push :finished_at, :output, :log + permitted.push :finished_at, *progress_attrs when Queued, Locked - permitted.push :finished_at, :log + permitted.push :finished_at, :log, :runtime_status end else @@ -459,6 +497,19 @@ class Container < ArvadosModel return false end + if self.state == Running && + !current_api_client_authorization.nil? && + (current_api_client_authorization.uuid == self.auth_uuid || + current_api_client_authorization.token == self.runtime_token) + # The contained process itself can write final attrs but can't + # change priority or log. + permitted.push *final_attrs + permitted = permitted - [:log, :priority] + elsif self.locked_by_uuid && self.locked_by_uuid != current_api_client_authorization.andand.uuid + # When locked, progress fields cannot be updated by the wrong + # dispatcher, even though it has admin privileges. + permitted = permitted - progress_attrs + end check_update_whitelist permitted end @@ -497,9 +548,22 @@ class Container < ArvadosModel end end + def update_cr_logs + # If self.final?, this update is superfluous: the final log/output + # update will be done when handle_completed calls finalize! on + # each requesting CR. + return if self.final? || !self.log_changed? + leave_modified_by_user_alone do + ContainerRequest.where(container_uuid: self.uuid).each do |cr| + cr.update_collections(container: self, collections: ['log']) + cr.save! + end + end + end + def assign_auth if self.auth_uuid_changed? - return errors.add :auth_uuid, 'is readonly' + return errors.add :auth_uuid, 'is readonly' end if not [Locked, Running].include? self.state # don't need one @@ -510,16 +574,29 @@ class Container < ArvadosModel # already have one return end - cr = ContainerRequest. - where('container_uuid=? and priority>0', self.uuid). - order('priority desc'). - first - if !cr - return errors.add :auth_uuid, "cannot be assigned because priority <= 0" + if self.runtime_token.nil? + if self.runtime_user_uuid.nil? + # legacy behavior, we don't have a runtime_user_uuid so get + # the user from the highest priority container request, needed + # when performing an upgrade and there are queued containers, + # and some tests. + cr = ContainerRequest. + where('container_uuid=? and priority>0', self.uuid). + order('priority desc'). + first + if !cr + return errors.add :auth_uuid, "cannot be assigned because priority <= 0" + end + self.runtime_user_uuid = cr.modified_by_user_uuid + self.runtime_auth_scopes = ["all"] + end + + # generate a new token + self.auth = ApiClientAuthorization. + create!(user_id: User.find_by_uuid(self.runtime_user_uuid).id, + api_client_id: 0, + scopes: self.runtime_auth_scopes) end - self.auth = ApiClientAuthorization. - create!(user_id: User.find_by_uuid(cr.modified_by_user_uuid).id, - api_client_id: 0) end def sort_serialized_attrs @@ -535,6 +612,9 @@ class Container < ArvadosModel if self.scheduling_parameters_changed? self.scheduling_parameters = self.class.deep_sort_hash(self.scheduling_parameters) end + if self.runtime_auth_scopes_changed? + self.runtime_auth_scopes = self.runtime_auth_scopes.sort + end end def update_secret_mounts_md5 @@ -544,12 +624,13 @@ class Container < ArvadosModel end end - def scrub_secret_mounts + def scrub_secrets # 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 = {} + self.runtime_token = nil end end @@ -581,7 +662,11 @@ class Container < ArvadosModel container_image: self.container_image, mounts: self.mounts, runtime_constraints: self.runtime_constraints, - scheduling_parameters: self.scheduling_parameters + scheduling_parameters: self.scheduling_parameters, + secret_mounts: self.secret_mounts_was, + runtime_token: self.runtime_token_was, + runtime_user_uuid: self.runtime_user_uuid, + runtime_auth_scopes: self.runtime_auth_scopes } c = Container.create! c_attrs retryable_requests.each do |cr|