Merge branch '13561-collection-versions-doc'
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 23 Oct 2018 13:56:18 +0000 (10:56 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 23 Oct 2018 13:56:18 +0000 (10:56 -0300)
Closes #13561

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

31 files changed:
apps/workbench/app/models/container_work_unit.rb
apps/workbench/app/views/work_units/_show_status.html.erb
doc/_config.yml
doc/admin/upgrade-crunch2.html.textile.liquid [new file with mode: 0644]
doc/api/methods/container_requests.html.textile.liquid
doc/api/methods/containers.html.textile.liquid
doc/user/cwl/cwl-style.html.textile.liquid
services/api/app/controllers/arvados/v1/containers_controller.rb
services/api/app/models/api_client_authorization.rb
services/api/app/models/arvados_model.rb
services/api/app/models/container.rb
services/api/app/models/container_request.rb
services/api/db/migrate/20181005192222_add_container_runtime_token.rb [new file with mode: 0644]
services/api/db/migrate/20181011184200_add_runtime_token_to_container.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/lib/sweep_trashed_objects.rb
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/collections.yml
services/api/test/fixtures/container_requests.yml
services/api/test/fixtures/containers.yml
services/api/test/fixtures/links.yml
services/api/test/fixtures/users.yml
services/api/test/functional/arvados/v1/collections_controller_test.rb
services/api/test/functional/arvados/v1/container_requests_controller_test.rb
services/api/test/functional/arvados/v1/containers_controller_test.rb
services/api/test/integration/container_auth_test.rb [new file with mode: 0644]
services/api/test/integration/remote_user_test.rb
services/api/test/unit/api_client_authorization_test.rb
services/api/test/unit/container_request_test.rb
services/api/test/unit/container_test.rb
services/crunch-run/crunchrun.go

index 964295619af4bf3b738b565ff401007fb203a72b..ef20a7f8f49cfd7ed72f7ff9c51774ca36ddaa74 100644 (file)
@@ -23,7 +23,7 @@ class ContainerWorkUnit < ProxyWorkUnit
     items = []
     container_uuid = if @proxied.is_a?(Container) then uuid else get(:container_uuid) end
     if container_uuid
-      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts)
+      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts secret_mounts runtime_token)
       my_children = @child_proxies || ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).results if !my_children
       my_child_containers = my_children.map(&:container_uuid).compact.uniq
       grandchildren = {}
index b726f3b6b1459b55bc54453ba3811eec464fa15d..003948584afae697bfef7822fdb60c65e5335674 100644 (file)
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <%
     container_uuid = if @object.is_a?(Container) then @object.uuid elsif @object.is_a?(ContainerRequest) then @object.container_uuid end
     if container_uuid
-      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts)
+      cols = ContainerRequest.columns.map(&:name) - %w(id updated_at mounts runtime_token)
       reqs = ContainerRequest.select(cols).where(requesting_container_uuid: container_uuid).results
       load_preloaded_objects(reqs)
 
index 921e261fb0ac7de5bcbbd992bfe6638843210392..017aa580d431476c39abe0892fbcfdb937c539c0 100644 (file)
@@ -153,6 +153,7 @@ navbar:
     - Upgrading and migrations:
       - admin/upgrading.html.textile.liquid
       - install/migrate-docker19.html.textile.liquid
+      - admin/upgrade-crunch2.html.textile.liquid
     - Users and Groups:
       - install/cheat_sheet.html.textile.liquid
       - admin/activation.html.textile.liquid
diff --git a/doc/admin/upgrade-crunch2.html.textile.liquid b/doc/admin/upgrade-crunch2.html.textile.liquid
new file mode 100644 (file)
index 0000000..1946358
--- /dev/null
@@ -0,0 +1,53 @@
+---
+layout: default
+navsection: admin
+title: Upgrading to Containers API
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+The "containers" API is the recommended way to submit compute work to Arvados.  It supersedes the "jobs" API, which is deprecated.
+
+h2. Benefits over the "jobs" API
+
+* Simpler and more robust execution with fewer points of failure
+* Automatic retry for containers that fail to run to completion due to infrastructure errors
+* Scales to thousands of simultaneous containers
+* Able to support alternate schedulers/dispatchers in addition to slurm
+* Improved logging, different streams logs/metrics stored in different files in the log collection
+* Records more upfront detail about the compute node, and additional metrics (such as available disk space over the course of the container run)
+* Better behavior when deciding whether to reuse past work -- pick the oldest container that matches the criteria
+* Can reuse running containers between workflows, cancelling a workflow will not cancel containers that are shared with other workflows
+* Supports setting time-to-live on intermediate output collections for automatic cleanup
+* Supports "secret" inputs, suitable for passwords or access tokens, which are hidden from the API responses and logs, and forgotten after use
+* Does not require "git" for dispatching work
+
+h2. Differences from the "jobs" API
+
+Containers cannot reuse jobs (but can reuse other containers)
+
+Uses the service "crunch-dispatch-slurm":{{site.baseurl}}/install/crunch2-slurm/install-dispatch.html instead of @crunch-dispatch.rb@
+
+Non-CWL Arvados "pipeline templates" are not supported with containers.  Pipeline templates should be rewritten in CWL and registered as "Workflows".
+
+The containers APIs is incompatible with the jobs API, code which integrates with the "jobs" API must be updated to work with containers
+
+Containers have network access disabled by default
+
+The keep mount only exposes collections which are explicitly listed as inputs
+
+h2. Migrating to "containers" API
+
+Run your workflows using @arvados-cwl-runner --api=containers@ (only necessary if both the jobs and containers APIs are enabled, if the jobs API is disabled, it will use the containers API automatically)
+
+Register your workflows so they can be run from workbench using @arvados-cwl-runner --api=containers --create-workflow@
+
+Read "Migrating running CWL on jobs API to containers API":{{site.baseurl}}/user/cwl/cwl-style.html#migrate
+
+Use @arv:APIRequirement: {}@ in the @requirements@ section of your CWL file to enable network access for the container (see "Arvados CWL Extensions":{{site.baseurl}}/user/cwl/cwl-extensions.html)
+
+For examples on how to manage container requests with the Python SDK, see "Python cookbook":{{site.baseurl}}/sdk/python/cookbook.html
index e1e006a86a1e63e669cb204975e3c64ab8849f3f..8703e927327dfb100a4dda73932e3740df2c6388 100644 (file)
@@ -35,6 +35,7 @@ table(table table-bordered table-condensed).
 |container_uuid|string|The uuid of the container that satisfies this container_request. The system may return a preexisting Container that matches the container request criteria. See "Container reuse":#container_reuse for more details.|Container reuse is the default behavior, but may be disabled with @use_existing: false@ to always create a new container.|
 |container_count_max|integer|Maximum number of containers to start, i.e., the maximum number of "attempts" to be made.||
 |mounts|hash|Objects to attach to the container's filesystem and stdin/stdout.|See "Mount types":#mount_types for more details.|
+|secret_mounts|hash|Objects to attach to the container's filesystem.  Only "json" or "text" mount types allowed.|Not returned in API responses. Reset to empty when state is "Complete" or "Cancelled".|
 |runtime_constraints|hash|Restrict the container's access to compute resources and the outside world.|Required when in "Committed" state. e.g.,<pre><code>{
   "ram":12000000000,
   "vcpus":2,
@@ -56,6 +57,9 @@ table(table table-bordered table-condensed).
 |log_uuid|string|Log collection containing log messages provided by the scheduler and crunch processes.|Null if the container has not yet completed.|
 |output_uuid|string|Output collection created when the container finished successfully.|Null if the container has failed or not yet completed.|
 |filters|string|Additional constraints for satisfying the container_request, given in the same form as the filters parameter accepted by the container_requests.list API.|
+|runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc.  |Not returned in API responses.  Reset to null when state is "Complete" or "Cancelled".|
+|runtime_user_uuid|string|The user permission that will be granted to this container.||
+|runtime_auth_scopes|array of string|The scopes associated with the auth token used to run this container.||
 
 h2(#priority). Priority
 
@@ -79,7 +83,7 @@ h2(#scheduling_parameters). {% include 'container_scheduling_parameters' %}
 
 h2(#container_reuse). Container reuse
 
-When a container request is "Committed", the system will try to find and reuse an existing Container with the same command, cwd, environment, output_path, container_image, mounts, and runtime_constraints being requested. (Hashes in the serialized fields environment, mounts and runtime_constraints are compared without regard to key order.)
+When a container request is "Committed", the system will try to find and reuse an existing Container with the same command, cwd, environment, output_path, container_image, mounts, secret_mounts, runtime_constraints, runtime_user_uuid, and runtime_auth_scopes being requested. (Hashes in the serialized fields environment, mounts and runtime_constraints use normalized key order.)
 
 In order of preference, the system will use:
 * The first matching container to have finished successfully (i.e., reached state "Complete" with an exit_code of 0) whose log and output collections are still available.
index 61e2715223493539eed2aa9a1b3ad355599354a8..f0ce8e362f40ee0c533829b3c04adf7afa6ccf88 100644 (file)
@@ -34,6 +34,7 @@ table(table table-bordered table-condensed).
 |command|array of strings|Command to execute.| Must be equal to a ContainerRequest's command in order to satisfy the ContainerRequest.|
 |output_path|string|Path to a directory or file inside the container that should be preserved as this container's output when it finishes.|Must be equal to a ContainerRequest's output_path in order to satisfy the ContainerRequest.|
 |mounts|hash|Must contain the same keys as the ContainerRequest being satisfied. Each value must be within the range of values described in the ContainerRequest at the time the Container is assigned to the ContainerRequest.|See "Mount types":#mount_types for more details.|
+|secret_mounts|hash|Must contain the same keys as the ContainerRequest being satisfied. Each value must be within the range of values described in the ContainerRequest at the time the Container is assigned to the ContainerRequest.|Not returned in API responses. Reset to empty when state is "Complete" or "Cancelled".|
 |runtime_constraints|hash|Compute resources, and access to the outside world, that are / were available to the container.
 Generally this will contain additional keys that are not present in any corresponding ContainerRequests: for example, even if no ContainerRequests specified constraints on the number of CPU cores, the number of cores actually used will be recorded here.|e.g.,
 <pre><code>{
@@ -53,8 +54,9 @@ Generally this will contain additional keys that are not present in any correspo
 |progress|number|A number between 0.0 and 1.0 describing the fraction of work done.||
 |priority|integer|Range 0-1000.  Indicate scheduling order preference.|Currently assigned by the system as the max() of the priorities of all associated ContainerRequests.  See "container request priority":container_requests.html#priority .|
 |exit_code|integer|Process exit code.|Null if state!="Complete"|
-|auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc.|Null if state∉{"Locked","Running"}|
+|auth_uuid|string|UUID of a token to be passed into the container itself, used to access Keep-backed mounts, etc.  Automatically assigned.|Null if state∉{"Locked","Running"} or if @runtime_token@ was provided.|
 |locked_by_uuid|string|UUID of a token, indicating which dispatch process changed state to Locked. If null, any token can be used to lock. If not null, only the indicated token can modify this container.|Null if state∉{"Locked","Running"}|
+|runtime_token|string|A v2 token to be passed into the container itself, used to access Keep-backed mounts, etc.|Not returned in API responses.  Reset to null when state is "Complete" or "Cancelled".|
 
 h2(#container_states). Container states
 
index 07cb4aa9095fad72e9854997427b5f171a941307..fe53f4a4548b40c0ecd076db36186118da3cb14d 100644 (file)
@@ -178,7 +178,8 @@ steps:
 
 h2(#migrate). Migrating running CWL on jobs API to containers API
 
-* When migrating from jobs API (--api=jobs) (sometimes referred to as "crunch v1") to the containers API (--api=containers) ("crunch v2") there are a few differences in behavior:
-** The tool is limited to accessing only collections which are explicitly listed in the input, and further limited to only the subdirectories of collections listed in input.  For example, given an explicit file input @/dir/subdir/file1.txt@, a tool will not be able to implicitly access the file @/dir/file2.txt@.  Use @secondaryFiles@ or a @Directory@ input to describe trees of files.
-** Files listed in @InitialWorkDirRequirement@ appear in the output directory as normal files (not symlinks) but cannot be moved, renamed or deleted.  These files will be added to the output collection but without any additional copies of the underlying data.
-** Tools are disallowed network access by default.  Tools which require network access must include @arv:APIRequirement: {}@ in their @requirements@ section.
+When migrating from jobs API (--api=jobs) (sometimes referred to as "crunch v1") to the containers API (--api=containers) ("crunch v2") there are a few differences in behavior:
+
+* A tool may fail to find an input file that could be found when run under the jobs API.  This is because tools are limited to accessing collections explicitly listed in the input, and further limited to those individual files or subdirectories that are listed.  For example, given an explicit file input @/dir/subdir/file1.txt@, a tool will not be allowed to implicitly access a file in the parent directory @/dir/file2.txt@.  Use @secondaryFiles@ or a @Directory@ for files that need to be grouped together.
+* A tool may fail when attempting to rename or delete a file in the output directory.  This may happen because files listed in @InitialWorkDirRequirement@ appear in the output directory as normal files (not symlinks) but cannot be moved, renamed or deleted unless marked as "writable" in CWL.  These files will be added to the output collection but without any additional copies of the underlying data.
+* A tool may fail when attempting to access the network.  This may happen because, unlike the jobs API, under the containers API network access is disabled by default.  Tools which require network access should add @arv:APIRequirement: {}@ to the @requirements@ section.
index 65d8385ad5f5b47619f1e158e674564333e09433..8542096ce1aae27ee4ffe927c7b11f0bc9a4ea11 100644 (file)
@@ -17,7 +17,14 @@ class Arvados::V1::ContainersController < ApplicationController
     if @object.locked_by_uuid != Thread.current[:api_client_authorization].uuid
       raise ArvadosModel::PermissionDeniedError.new("Not locked by your token")
     end
-    @object = @object.auth
+    if @object.runtime_token.nil?
+      @object = @object.auth
+    else
+      @object = ApiClientAuthorization.validate(token: @object.runtime_token)
+      if @object.nil?
+        raise ArvadosModel::PermissionDeniedError.new("Invalid runtime_token")
+      end
+    end
     show
   end
 
@@ -51,20 +58,18 @@ class Arvados::V1::ContainersController < ApplicationController
     if Thread.current[:api_client_authorization].nil?
       send_error("Not logged in", status: 401)
     else
-      c = Container.where(auth_uuid: Thread.current[:api_client_authorization].uuid).first
-      if c.nil?
+      @object = Container.for_current_token
+      if @object.nil?
         send_error("Token is not associated with a container.", status: 404)
       else
-        @object = c
         show
       end
     end
   end
 
   def secret_mounts
-    if @object &&
-       @object.auth_uuid &&
-       @object.auth_uuid == Thread.current[:api_client_authorization].uuid
+    c = Container.for_current_token
+    if @object && c && @object.uuid == c.uuid
       send_json({"secret_mounts" => @object.secret_mounts})
     else
       send_error("Token is not associated with this container.", status: 403)
index 12ef8eb3eb5a2abede54c35919a8b72c815a357c..53ae6af46426cadd55bf7ec4ae1cc94659ef1c0f 100644 (file)
@@ -98,11 +98,31 @@ class ApiClientAuthorization < ArvadosModel
 
     case token[0..2]
     when 'v2/'
-      _, uuid, secret = token.split('/')
+      _, uuid, secret, optional = token.split('/')
       unless uuid.andand.length == 27 && secret.andand.length.andand > 0
         return nil
       end
 
+      if !optional.nil?
+        # if "optional" is a container uuid, check that it
+        # matches expections.
+        c = Container.where(uuid: optional).first
+        if !c.nil?
+          if !c.auth_uuid.nil? and c.auth_uuid != uuid
+            # token doesn't match the container's token
+            return nil
+          end
+          if !c.runtime_token.nil? and "v2/#{uuid}/#{secret}" != c.runtime_token
+            # token doesn't match the container's token
+            return nil
+          end
+          if ![Container::Locked, Container::Running].include?(c.state)
+            # container isn't locked or running, token shouldn't be used
+            return nil
+          end
+        end
+      end
+
       auth = ApiClientAuthorization.
              includes(:user, :api_client).
              where('uuid=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', uuid).
index 801da17dbee5455e33991a64d542d4ff9eaad1da..cc15a56f35325f56ea5762c050aa4494f5e5a5d4 100644 (file)
@@ -269,11 +269,6 @@ class ArvadosModel < ActiveRecord::Base
       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
     end
 
-    exclude_old_versions = ""
-    if !include_old_versions && sql_table == "collections"
-      exclude_old_versions = "AND #{sql_table}.uuid = #{sql_table}.current_version_uuid"
-    end
-
     if users_list.select { |u| u.is_admin }.any?
       # Admin skips most permission checks, but still want to filter on trashed items.
       if !include_trash
@@ -281,7 +276,7 @@ class ArvadosModel < ActiveRecord::Base
           # Only include records where the owner is not trashed
           sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
                       "WHERE trashed = 1 AND "+
-                      "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records} #{exclude_old_versions}"
+                      "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
         end
       end
     else
@@ -318,8 +313,17 @@ class ArvadosModel < ActiveRecord::Base
                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
       end
 
-      sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records} #{exclude_old_versions}"
+      sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
+
+    end
 
+    if !include_old_versions && sql_table == "collections"
+      exclude_old_versions = "#{sql_table}.uuid = #{sql_table}.current_version_uuid"
+      if sql_conds.nil?
+        sql_conds = exclude_old_versions
+      else
+        sql_conds += " AND #{exclude_old_versions}"
+      end
     end
 
     self.where(sql_conds,
index 079ac4c29980406982ee849046c4e23dccbe7e5f..0d8453174e205e85ab3f79e01a32cc530478a4a1 100644 (file)
@@ -37,7 +37,7 @@ 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
@@ -67,6 +67,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
@@ -91,15 +93,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
@@ -146,17 +148,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
@@ -259,6 +281,14 @@ class Container < ArvadosModel
     candidates = candidates.where_serialized(:runtime_constraints, resolve_runtime_constraints(attrs[:runtime_constraints]), md5: true)
     log_reuse_info(candidates) { "after filtering on runtime_constraints #{attrs[:runtime_constraints].inspect}" }
 
+    candidates = candidates.where('runtime_user_uuid = ? or (runtime_user_uuid is NULL and runtime_auth_scopes is NULL)',
+                                  attrs[:runtime_user_uuid])
+    log_reuse_info(candidates) { "after filtering on runtime_user_uuid #{attrs[:runtime_user_uuid].inspect}" }
+
+    candidates = candidates.where('runtime_auth_scopes = ? or (runtime_user_uuid is NULL and runtime_auth_scopes is NULL)',
+                                  SafeJSON.dump(attrs[:runtime_auth_scopes].sort))
+    log_reuse_info(candidates) { "after filtering on runtime_auth_scopes #{attrs[:runtime_auth_scopes].inspect}" }
+
     log_reuse_info { "checking for state=Complete with readable output and log..." }
 
     select_readable_pdh = Collection.
@@ -362,6 +392,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
@@ -415,7 +458,8 @@ class Container < ArvadosModel
       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
@@ -511,7 +555,7 @@ class Container < ArvadosModel
 
   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
@@ -522,16 +566,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
@@ -547,6 +604,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
@@ -556,12 +616,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
 
@@ -593,7 +654,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|
index bbec4210846ca95e918611fd23189e03f7433893..0c2ad096557d3f335fa398946a9c8dc1012044e2 100644 (file)
@@ -38,7 +38,8 @@ class ContainerRequest < ArvadosModel
   validate :validate_state_change
   validate :check_update_whitelist
   validate :secret_mounts_key_conflict
-  before_save :scrub_secret_mounts
+  validate :validate_runtime_token
+  before_save :scrub_secrets
   before_create :set_requesting_container_uuid
   before_destroy :set_priority_zero
   after_save :update_priority
@@ -88,7 +89,7 @@ class ContainerRequest < ArvadosModel
   AttrsPermittedAlways = [:owner_uuid, :state, :name, :description, :properties]
   AttrsPermittedBeforeCommit = [:command, :container_count_max,
   :container_image, :cwd, :environment, :filters, :mounts,
-  :output_path, :priority,
+  :output_path, :priority, :runtime_token,
   :runtime_constraints, :state, :container_uuid, :use_existing,
   :scheduling_parameters, :secret_mounts, :output_name, :output_ttl]
 
@@ -97,7 +98,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def logged_attributes
-    super.except('secret_mounts')
+    super.except('secret_mounts', 'runtime_token')
   end
 
   def state_transitions
@@ -105,8 +106,12 @@ class ContainerRequest < ArvadosModel
   end
 
   def skip_uuid_read_permission_check
-    # XXX temporary until permissions are sorted out.
-    %w(modified_by_client_uuid container_uuid requesting_container_uuid)
+    # The uuid_read_permission_check prevents users from making
+    # references to objects they can't view.  However, in this case we
+    # don't want to do that check since there's a circular dependency
+    # where user can't view the container until the user has
+    # constructed the container request that references the container.
+    %w(container_uuid)
   end
 
   def finalize_if_needed
@@ -165,7 +170,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["mounts", "secret_mounts", "secret_mounts_md5"]
+    super - ["mounts", "secret_mounts", "secret_mounts_md5", "runtime_token"]
   end
 
   protected
@@ -343,9 +348,22 @@ class ContainerRequest < ArvadosModel
     end
   end
 
-  def scrub_secret_mounts
+  def validate_runtime_token
+    if !self.runtime_token.nil? && self.runtime_token_changed?
+      if !runtime_token[0..2] == "v2/"
+        errors.add :runtime_token, "not a v2 token"
+        return
+      end
+      if ApiClientAuthorization.validate(token: runtime_token).nil?
+        errors.add :runtime_token, "failed validation"
+      end
+    end
+  end
+
+  def scrub_secrets
     if self.state == Final
       self.secret_mounts = {}
+      self.runtime_token = nil
     end
   end
 
@@ -374,9 +392,6 @@ class ContainerRequest < ArvadosModel
 
   def get_requesting_container
     return self.requesting_container_uuid if !self.requesting_container_uuid.nil?
-    return if !current_api_client_authorization
-    if (c = Container.where('auth_uuid=?', current_api_client_authorization.uuid).select([:uuid, :priority]).first)
-      return c
-    end
+    Container.for_current_token
   end
 end
diff --git a/services/api/db/migrate/20181005192222_add_container_runtime_token.rb b/services/api/db/migrate/20181005192222_add_container_runtime_token.rb
new file mode 100644 (file)
index 0000000..07151cd
--- /dev/null
@@ -0,0 +1,7 @@
+class AddContainerRuntimeToken < ActiveRecord::Migration
+  def change
+    add_column :container_requests, :runtime_token, :text, :null => true
+    add_column :containers, :runtime_user_uuid, :text, :null => true
+    add_column :containers, :runtime_auth_scopes, :jsonb, :null => true
+  end
+end
diff --git a/services/api/db/migrate/20181011184200_add_runtime_token_to_container.rb b/services/api/db/migrate/20181011184200_add_runtime_token_to_container.rb
new file mode 100644 (file)
index 0000000..09201f5
--- /dev/null
@@ -0,0 +1,5 @@
+class AddRuntimeTokenToContainer < ActiveRecord::Migration
+  def change
+    add_column :containers, :runtime_token, :text, :null => true
+  end
+end
index 5b579bd39e1e8760cd3243bf7dbe50b206d131f4..5105914df0dbd04ab599790d934f03194021dccf 100644 (file)
@@ -302,7 +302,8 @@ CREATE TABLE public.container_requests (
     log_uuid character varying(255),
     output_name character varying(255) DEFAULT NULL::character varying,
     output_ttl integer DEFAULT 0 NOT NULL,
-    secret_mounts jsonb DEFAULT '{}'::jsonb
+    secret_mounts jsonb DEFAULT '{}'::jsonb,
+    runtime_token text
 );
 
 
@@ -358,7 +359,10 @@ CREATE TABLE public.containers (
     scheduling_parameters text,
     secret_mounts jsonb DEFAULT '{}'::jsonb,
     secret_mounts_md5 character varying DEFAULT '99914b932bd37a50b983c5e7c90ae93b'::character varying,
-    runtime_status jsonb DEFAULT '{}'::jsonb
+    runtime_status jsonb DEFAULT '{}'::jsonb,
+    runtime_user_uuid text,
+    runtime_auth_scopes jsonb,
+    runtime_token text
 );
 
 
@@ -3190,3 +3194,8 @@ INSERT INTO schema_migrations (version) VALUES ('20180919001158');
 INSERT INTO schema_migrations (version) VALUES ('20181001175023');
 
 INSERT INTO schema_migrations (version) VALUES ('20181004131141');
+
+INSERT INTO schema_migrations (version) VALUES ('20181005192222');
+
+INSERT INTO schema_migrations (version) VALUES ('20181011184200');
+
index 59008c0fc38067a3bf3ece9a885c0bfebfa2a438..bedbd68a44c8a9e988c202a21457281f680e840a 100644 (file)
@@ -48,6 +48,9 @@ module SweepTrashedObjects
         where({group_class: 'project'}).
         where('is_trashed = false and trash_at < statement_timestamp()').
         update_all('is_trashed = true')
+
+      # Sweep expired tokens
+      ActiveRecord::Base.connection.execute("DELETE from api_client_authorizations where expires_at <= statement_timestamp()")
     end
   end
 
index 2073d8b1bacccfaa0422643a34ddfe5ed0144461..d8ef63120bfc2a2eb6d938d0ed217cee2f5d7144 100644 (file)
@@ -341,3 +341,25 @@ foo_collection_sharing_token:
   - GET /arvados/v1/collections/zzzzz-4zz18-znfnqtbbv4spc3w
   - GET /arvados/v1/collections/zzzzz-4zz18-znfnqtbbv4spc3w/
   - GET /arvados/v1/keep_services/accessible
+
+container_runtime_token:
+  uuid: zzzzz-gj3su-2nj68s291f50gd9
+  api_client: untrusted
+  user: container_runtime_token_user
+  api_token: 2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+  expires_at: 2038-01-01 00:00:00
+
+crt_user:
+  uuid: zzzzz-gj3su-3r47qqy5ja5d54v
+  api_client: untrusted
+  user: container_runtime_token_user
+  api_token: 13z1tz9deoryml3twep0vsahi4862097pe5lsmesugnkgpgpwk
+  expires_at: 2038-01-01 00:00:00
+
+runtime_token_limited_scope:
+  uuid: zzzzz-gj3su-2fljvypjrr4yr9m
+  api_client: untrusted
+  user: container_runtime_token_user
+  api_token: 1fwc3be1m13qkypix2gd01i4bq5ju483zjfc0cf4babjseirbm
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /"]
index 29b486c452d4b01a353f364e2672dda2c2f5cee1..8763f3944471e9a5cad4f4565da2833f988bbdf8 100644 (file)
@@ -291,6 +291,24 @@ expired_collection:
   delete_at: 2038-01-01T00:00:00Z
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:expired\n"
   name: expired_collection
+  version: 2
+
+expired_collection_past_version:
+  uuid: zzzzz-4zz18-mto52zx1s7oldie
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3ih
+  portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2014-02-03T17:12:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2014-02-03T17:17:54Z
+  updated_at: 2014-02-03T17:17:54Z
+  is_trashed: true
+  trash_at: 2001-01-01T00:00:00Z
+  delete_at: 2038-01-01T00:00:00Z
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:expired\n"
+  name: expired_collection original
+  version: 1
 
 trashed_on_next_sweep:
   uuid: zzzzz-4zz18-4guozfh77ewd2f0
index 5d3531eead8fb5a90c7ef4b7ef750a937da6ee90..dea98887e9843866b182a7ca054aa60628223fd7 100644 (file)
@@ -764,6 +764,26 @@ cr_in_trashed_project:
     vcpus: 1
     ram: 123
 
+runtime_token:
+  uuid: zzzzz-xvhdp-11eklkhy0n4dm86
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  name: queued
+  state: Committed
+  priority: 1
+  created_at: <%= 2.minute.ago.to_s(:db) %>
+  updated_at: <%= 1.minute.ago.to_s(:db) %>
+  modified_at: <%= 1.minute.ago.to_s(:db) %>
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  container_uuid: zzzzz-dz642-20isqbkl8xwnsao
+  runtime_token: v2/zzzzz-gj3su-2nj68s291f50gd9/2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+  runtime_constraints:
+    vcpus: 1
+    ram: 123
+
 
 # Test Helper trims the rest of the file
 
index 757adcee1b979af4086d937cc928c1abb5042a1e..5c5d45f4bc0c5a880775d638bc133752992a454d 100644 (file)
@@ -259,3 +259,29 @@ running_to_be_deleted:
   auth_uuid: zzzzz-gj3su-ty6lvu9d7u7c2sq
   secret_mounts: {}
   secret_mounts_md5: 99914b932bd37a50b983c5e7c90ae93b
+
+runtime_token:
+  uuid: zzzzz-dz642-20isqbkl8xwnsao
+  owner_uuid: zzzzz-tpzed-000000000000000
+  state: Locked
+  locked_by_uuid: zzzzz-gj3su-jrriu629zljsnuf
+  priority: 1
+  created_at: 2016-01-11 11:11:11.111111111 Z
+  updated_at: 2016-01-11 11:11:11.111111111 Z
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  runtime_token: v2/zzzzz-gj3su-2nj68s291f50gd9/2d19ue6ofx26o3mm7fs9u6t7hov9um0v92dzwk1o2xed3abprw
+  runtime_user_uuid: zzzzz-tpzed-l3skomkti0c4vg4
+  runtime_auth_scopes: ["all"]
+  runtime_constraints:
+    ram: 12000000000
+    vcpus: 4
+  mounts:
+    /tmp:
+      kind: tmp
+      capacity: 24000000000
+    /var/spool/cwl:
+      kind: tmp
+      capacity: 24000000000
index 8a33f696a958e56330aaa4cf6c9bbd0e19624ec4..2b247a960d989e962b373b726878828bc008d105 100644 (file)
@@ -597,6 +597,20 @@ active_user_permission_to_unlinked_docker_image_collection:
   head_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
   properties: {}
 
+crt_user_permission_to_unlinked_docker_image_collection:
+  uuid: zzzzz-o0j2j-20zvdi9b4odcfz3
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2014-01-24 20:42:26 -0800
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-000000000000000
+  modified_at: 2014-01-24 20:42:26 -0800
+  updated_at: 2014-01-24 20:42:26 -0800
+  tail_uuid: zzzzz-tpzed-l3skomkti0c4vg4
+  link_class: permission
+  name: can_read
+  head_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
+  properties: {}
+
 docker_image_collection_hash:
   uuid: zzzzz-o0j2j-dockercollhasha
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
index 8d2586921958570d97104b3fdd8bcefb8e51112f..7d6b1fc3aef2a6a7e5d4b95dffff1c63100d15bb 100644 (file)
@@ -165,6 +165,22 @@ spectator:
       role: Computational biologist
     getting_started_shown: 2015-03-26 12:34:56.789000000 Z
 
+container_runtime_token_user:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-l3skomkti0c4vg4
+  email: spectator@arvados.local
+  first_name: Spect
+  last_name: Ator
+  identity_url: https://spectator.openid.local
+  is_active: true
+  is_admin: false
+  username: containerruntimetokenuser
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
+    getting_started_shown: 2015-03-26 12:34:56.789000000 Z
+
 inactive_uninvited:
   owner_uuid: zzzzz-tpzed-000000000000000
   uuid: zzzzz-tpzed-rf2ec3ryh4vb5ma
index f5bed638497d0f9c62d6d130cf314576cb4c2685..26b8290e6961452e97f505ad3b239f6ef5a28596 100644 (file)
@@ -1025,6 +1025,54 @@ EOS
     assert_response 200
   end
 
+  [:admin, :active].each do |user|
+    test "get trashed collection via filters and #{user} user" do
+      uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
+      authorize_with user
+      get :index, {
+        filters: [["current_version_uuid", "=", uuid]],
+        include_trash: true,
+      }
+      assert_response 200
+      # Only the current version is returned
+      assert_equal 1, json_response["items"].size
+    end
+  end
+
+  [:admin, :active].each do |user|
+    test "get trashed collection via filters and #{user} user, including its past versions" do
+      uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
+      authorize_with :admin
+      get :index, {
+        filters: [["current_version_uuid", "=", uuid]],
+        include_trash: true,
+        include_old_versions: true,
+      }
+      assert_response 200
+      # Both current & past version are returned
+      assert_equal 2, json_response["items"].size
+    end
+  end
+
+  test "trash collection also trash its past versions" do
+    uuid = collections(:collection_owned_by_active).uuid
+    authorize_with :active
+    versions = Collection.where(current_version_uuid: uuid)
+    assert_equal 2, versions.size
+    versions.each do |col|
+      refute col.is_trashed
+    end
+    post :trash, {
+      id: uuid,
+    }
+    assert_response 200
+    versions = Collection.where(current_version_uuid: uuid)
+    assert_equal 2, versions.size
+    versions.each do |col|
+      assert col.is_trashed
+    end
+  end
+
   test 'get trashed collection without include_trash' do
     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
     authorize_with :active
index 282e09049e63beab2e591ac71f38b47e9484261d..a3252ad7b3fcdaa8fc78294bbb29b794a7107986 100644 (file)
@@ -81,4 +81,21 @@ class Arvados::V1::ContainerRequestsControllerTest < ActionController::TestCase
     req.reload
     assert_equal 'bar', req.secret_mounts['/foo']['content']
   end
+
+  test "runtime_token not in #create responses" do
+    authorize_with :active
+
+    post :create, {
+           container_request: minimal_cr.merge(
+             runtime_token: api_client_authorizations(:spectator).token)
+         }
+    assert_response :success
+
+    resp = JSON.parse(@response.body)
+    refute resp.has_key?('runtime_token')
+
+    req = ContainerRequest.where(uuid: resp['uuid']).first
+    assert_equal api_client_authorizations(:spectator).token, req.runtime_token
+  end
+
 end
index 8e2002c75919a68f27b64718e50279907339ce7d..452533b9e9a13e93b05f4c28be484e5c8ca23f98 100644 (file)
@@ -151,4 +151,14 @@ class Arvados::V1::ContainersControllerTest < ActionController::TestCase
       end
     end
   end
+
+  test 'get runtime_token auth' do
+    authorize_with :dispatch2
+    c = containers(:runtime_token)
+    get :auth, id: c.uuid
+    assert_response :success
+    assert_equal "v2/#{json_response['uuid']}/#{json_response['api_token']}", api_client_authorizations(:container_runtime_token).token
+    assert_equal 'arvados#apiClientAuthorization', json_response['kind']
+  end
+
 end
diff --git a/services/api/test/integration/container_auth_test.rb b/services/api/test/integration/container_auth_test.rb
new file mode 100644 (file)
index 0000000..552cce4
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'test_helper'
+
+class ContainerAuthTest < ActionDispatch::IntegrationTest
+  fixtures :all
+
+  test "container token validate, Running, regular auth" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:running_container_auth).token}/#{containers(:running).uuid}"}
+    # Container is Running, token can be used
+    assert_response :success
+    assert_equal containers(:running).uuid, json_response['uuid']
+  end
+
+  test "container token validate, Locked, runtime_token" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:runtime_token).uuid}"}
+    # Container is Running, token can be used
+    assert_response :success
+    assert_equal containers(:runtime_token).uuid, json_response['uuid']
+  end
+
+  test "container token validate, Cancelled, runtime_token" do
+    put "/arvados/v1/containers/#{containers(:runtime_token).uuid}", {
+          :format => :json,
+          :container => {:state => "Cancelled"}
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:dispatch1).token}"}
+    assert_response :success
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:runtime_token).uuid}"}
+    # Container is Queued, token cannot be used
+    assert_response 401
+  end
+
+  test "container token validate, Running, without optional portion" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:running_container_auth).token}"}
+    # Container is Running, token can be used
+    assert_response :success
+    assert_equal containers(:running).uuid, json_response['uuid']
+  end
+
+  test "container token validate, Locked, runtime_token, without optional portion" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}"}
+    # runtime_token without container uuid won't return 'current'
+    assert_response 404
+  end
+
+  test "container token validate, wrong container uuid" do
+    get "/arvados/v1/containers/current", {
+      :format => :json
+        }, {'HTTP_AUTHORIZATION' => "Bearer #{api_client_authorizations(:container_runtime_token).token}/#{containers(:running).uuid}"}
+    # Container uuid mismatch, token can't be used
+    assert_response 401
+  end
+end
index c38c230b2276609c6ce21ccf581f4e710854167d..0e61db7bcd9d5cc0cb185c4766a2e597c6d6ed4a 100644 (file)
@@ -251,4 +251,37 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     assert_equal 'barney', json_response['username']
   end
 
+  test "validate unsalted v2 token for remote cluster zbbbb" do
+    auth = api_client_authorizations(:active)
+    token = "v2/#{auth.uuid}/#{auth.api_token}"
+    get '/arvados/v1/users/current', {format: 'json', remote: 'zbbbb'}, {
+          "HTTP_AUTHORIZATION" => "Bearer #{token}"
+        }
+    assert_response :success
+    assert_equal(users(:active).uuid, json_response['uuid'])
+  end
+
+  test 'container request with runtime_token' do
+    [["valid local", "v2/#{api_client_authorizations(:active).uuid}/#{api_client_authorizations(:active).api_token}"],
+     ["valid remote", "v2/zbbbb-gj3su-000000000000000/abc"],
+     ["invalid local", "v2/#{api_client_authorizations(:active).uuid}/fakefakefake"],
+     ["invalid remote", "v2/zbork-gj3su-000000000000000/abc"],
+    ].each do |label, runtime_token|
+      post '/arvados/v1/container_requests', {
+             "container_request" => {
+               "command" => ["echo"],
+               "container_image" => "xyz",
+               "output_path" => "/",
+               "cwd" => "/",
+               "runtime_token" => runtime_token
+             }
+           }, {"HTTP_AUTHORIZATION" => "Bearer #{api_client_authorizations(:active).api_token}"}
+      if label.include? "invalid"
+        assert_response 422
+      else
+        assert_response :success
+      end
+    end
+  end
+
 end
index 51a6ff3ba857821bacf831b6a08cf3804b4dc316..c390a02c04ef1ce705fa23f7a26aa2a42a93b51b 100644 (file)
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 require 'test_helper'
+require 'sweep_trashed_objects'
 
 class ApiClientAuthorizationTest < ActiveSupport::TestCase
   include CurrentApiClient
@@ -18,4 +19,11 @@ class ApiClientAuthorizationTest < ActiveSupport::TestCase
       assert_empty ApiClientAuthorization.where(api_token: newtoken), "Destroyed ApiClientAuth is still in database"
     end
   end
+
+  test "delete expired in SweepTrashedObjects" do
+    assert_not_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+    SweepTrashedObjects.sweep_now
+    assert_empty ApiClientAuthorization.where(uuid: api_client_authorizations(:expired).uuid)
+  end
+
 end
index 81b49ff4fcce525b5e7fba88ff0c6f78087e7686..8ff216e28caf8a598c5b6fbbf46a9d342e4a7c35 100644 (file)
@@ -380,7 +380,7 @@ class ContainerRequestTest < ActiveSupport::TestCase
 
   [
     ['running_container_auth', 'zzzzz-dz642-runningcontainr', 501],
-    ['active_no_prefs', nil, 0],
+    ['active_no_prefs', nil, 0]
   ].each do |token, expected, expected_priority|
     test "create as #{token} and expect requesting_container_uuid to be #{expected}" do
       set_user_from_auth token
@@ -391,6 +391,15 @@ class ContainerRequestTest < ActiveSupport::TestCase
     end
   end
 
+  test "create as container_runtime_token and expect requesting_container_uuid to be zzzzz-dz642-20isqbkl8xwnsao" do
+    set_user_from_auth :container_runtime_token
+    Thread.current[:token] = "#{Thread.current[:token]}/zzzzz-dz642-20isqbkl8xwnsao"
+    cr = ContainerRequest.create(container_image: "img", output_path: "/tmp", command: ["echo", "foo"])
+    assert_not_nil cr.uuid, 'uuid should be set for newly created container_request'
+    assert_equal 'zzzzz-dz642-20isqbkl8xwnsao', cr.requesting_container_uuid
+    assert_equal 1, cr.priority
+  end
+
   [[{"vcpus" => [2, nil]},
     lambda { |resolved| resolved["vcpus"] == 2 }],
    [{"vcpus" => [3, 7]},
@@ -668,6 +677,49 @@ class ContainerRequestTest < ActiveSupport::TestCase
     assert_not_equal cr2.container_uuid, cr.container_uuid
   end
 
+  test "Retry on container cancelled with runtime_token" do
+    set_user_from_auth :spectator
+    spec = api_client_authorizations(:active)
+    cr = create_minimal_req!(priority: 1, state: "Committed",
+                             runtime_token: spec.token,
+                             container_count_max: 2)
+    prev_container_uuid = cr.container_uuid
+
+    c = act_as_system_user do
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal spec.token, c.runtime_token
+      c.update_attributes!(state: Container::Locked)
+      c.update_attributes!(state: Container::Running)
+      c
+    end
+
+    cr.reload
+    assert_equal "Committed", cr.state
+    assert_equal prev_container_uuid, cr.container_uuid
+    prev_container_uuid = cr.container_uuid
+
+    act_as_system_user do
+      c.update_attributes!(state: Container::Cancelled)
+    end
+
+    cr.reload
+    assert_equal "Committed", cr.state
+    assert_not_equal prev_container_uuid, cr.container_uuid
+    prev_container_uuid = cr.container_uuid
+
+    c = act_as_system_user do
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal spec.token, c.runtime_token
+      c.update_attributes!(state: Container::Cancelled)
+      c
+    end
+
+    cr.reload
+    assert_equal "Final", cr.state
+    assert_equal prev_container_uuid, cr.container_uuid
+
+  end
+
   test "Output collection name setting using output_name with name collision resolution" do
     set_user_from_auth :active
     output_name = 'unimaginative name'
@@ -1074,4 +1126,38 @@ class ContainerRequestTest < ActiveSupport::TestCase
                                              secret_mounts: sm)
     assert_equal [:secret_mounts], cr.errors.messages.keys
   end
+
+  test "using runtime_token" do
+    set_user_from_auth :spectator
+    spec = api_client_authorizations(:active)
+    cr = create_minimal_req!(state: "Committed", runtime_token: spec.token, priority: 1)
+    cr.save!
+    c = Container.find_by_uuid cr.container_uuid
+    lock_and_run c
+    assert_nil c.auth_uuid
+    assert_equal c.runtime_token, spec.token
+
+    assert_not_nil ApiClientAuthorization.find_by_uuid(spec.uuid)
+
+    act_as_system_user do
+      c.update_attributes!(state: Container::Complete,
+                           exit_code: 0,
+                           output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45',
+                           log: 'fa7aeb5140e2848d39b416daeef4ffc5+45')
+    end
+
+    cr.reload
+    c.reload
+    assert_nil cr.runtime_token
+    assert_nil c.runtime_token
+  end
+
+  test "invalid runtime_token" do
+    set_user_from_auth :active
+    spec = api_client_authorizations(:spectator)
+    assert_raises(ArgumentError) do
+      cr = create_minimal_req!(state: "Committed", runtime_token: "#{spec.token}xx")
+      cr.save!
+    end
+  end
 end
index 11ae0bfe3b6b8d51e5c33555cbbbb8ed73128153..491022ad8d5a9cd6e47e1cf7727a5cba92d54ce4 100644 (file)
@@ -33,14 +33,18 @@ class ContainerTest < ActiveSupport::TestCase
       "var" => "val",
     },
     secret_mounts: {},
+    runtime_user_uuid: "zzzzz-tpzed-xurymjxw79nv3jz",
+    runtime_auth_scopes: ["all"]
   }
 
+  def request_only attrs
+    attrs.reject {|k| [:runtime_user_uuid, :runtime_auth_scopes].include? k}
+  end
+
   def minimal_new attrs={}
-    cr = ContainerRequest.new DEFAULT_ATTRS.merge(attrs)
+    cr = ContainerRequest.new request_only(DEFAULT_ATTRS.merge(attrs))
     cr.state = ContainerRequest::Committed
-    act_as_user users(:active) do
-      cr.save!
-    end
+    cr.save!
     c = Container.find_by_uuid cr.container_uuid
     assert_not_nil c
     return c, cr
@@ -220,6 +224,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container serialized hash attributes sorted before save" do
+    set_user_from_auth :active
     env = {"C" => "3", "B" => "2", "A" => "1"}
     m = {"F" => {"kind" => "3"}, "E" => {"kind" => "2"}, "D" => {"kind" => "1"}}
     rc = {"vcpus" => 1, "ram" => 1, "keep_cache_ram" => 1}
@@ -236,6 +241,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "find_reusable method should select higher priority queued container" do
+        Rails.configuration.log_reuse_decisions = true
     set_user_from_auth :active
     common_attrs = REUSABLE_COMMON_ATTRS.merge({environment:{"var" => "queued"}})
     c_low_priority, _ = minimal_new(common_attrs.merge({use_existing:false, priority:1}))
@@ -285,13 +291,13 @@ class ContainerTest < ActiveSupport::TestCase
       log: 'ea10d51bcf88862dbcc36eb292017dfd+45',
     }
 
-    cr = ContainerRequest.new common_attrs
+    cr = ContainerRequest.new request_only(common_attrs)
     cr.use_existing = false
     cr.state = ContainerRequest::Committed
     cr.save!
     c_output1 = Container.where(uuid: cr.container_uuid).first
 
-    cr = ContainerRequest.new common_attrs
+    cr = ContainerRequest.new request_only(common_attrs)
     cr.use_existing = false
     cr.state = ContainerRequest::Committed
     cr.save!
@@ -312,7 +318,8 @@ class ContainerTest < ActiveSupport::TestCase
     c_output2.update_attributes!({state: Container::Running})
     c_output2.update_attributes!(completed_attrs.merge({log: log1, output: out2}))
 
-    reused = Container.resolve(ContainerRequest.new(common_attrs))
+    set_user_from_auth :active
+    reused = Container.resolve(ContainerRequest.new(request_only(common_attrs)))
     assert_equal c_output1.uuid, reused.uuid
   end
 
@@ -507,7 +514,73 @@ class ContainerTest < ActiveSupport::TestCase
     Container.find_reusable(REUSABLE_COMMON_ATTRS)
   end
 
+  def runtime_token_attr tok
+    auth = api_client_authorizations(tok)
+    {runtime_user_uuid: User.find_by_id(auth.user_id).uuid,
+     runtime_auth_scopes: auth.scopes,
+     runtime_token: auth.token}
+  end
+
+  test "find_reusable method with same runtime_token" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:container_runtime_token).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_not_nil reused
+    assert_equal reused.uuid, c1.uuid
+  end
+
+  test "find_reusable method with different runtime_token, same user" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:crt_user).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_not_nil reused
+    assert_equal reused.uuid, c1.uuid
+  end
+
+  test "find_reusable method with nil runtime_token, then runtime_token with same user" do
+    set_user_from_auth :crt_user
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs)
+    assert_equal Container::Queued, c1.state
+    assert_equal users(:container_runtime_token_user).uuid, c1.runtime_user_uuid
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_not_nil reused
+    assert_equal reused.uuid, c1.uuid
+  end
+
+  test "find_reusable method with different runtime_token, different user" do
+    set_user_from_auth :crt_user
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:active).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_nil reused
+  end
+
+  test "find_reusable method with nil runtime_token, then runtime_token with different user" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: nil}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_nil reused
+  end
+
+  test "find_reusable method with different runtime_token, different scope, same user" do
+    set_user_from_auth :active
+    common_attrs = REUSABLE_COMMON_ATTRS.merge({use_existing:false, priority:1, environment:{"var" => "queued"}})
+    c1, _ = minimal_new(common_attrs.merge({runtime_token: api_client_authorizations(:runtime_token_limited_scope).token}))
+    assert_equal Container::Queued, c1.state
+    reused = Container.find_reusable(common_attrs.merge(runtime_token_attr(:container_runtime_token)))
+    assert_nil reused
+  end
+
   test "Container running" do
+    set_user_from_auth :active
     c, _ = minimal_new priority: 1
 
     set_user_from_auth :dispatch1
@@ -527,6 +600,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Lock and unlock" do
+    set_user_from_auth :active
     c, cr = minimal_new priority: 0
 
     set_user_from_auth :dispatch1
@@ -587,6 +661,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container queued cancel" do
+    set_user_from_auth :active
     c, cr = minimal_new({container_count_max: 1})
     set_user_from_auth :dispatch1
     assert c.update_attributes(state: Container::Cancelled), show_errors(c)
@@ -600,6 +675,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container locked cancel" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     assert c.lock, show_errors(c)
@@ -608,6 +684,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container locked cancel with log" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     assert c.lock, show_errors(c)
@@ -619,6 +696,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container running cancel" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -641,6 +719,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "Container only set exit code on complete" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -653,6 +732,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "locked_by_uuid can update log when locked/running, and output when running" do
+    set_user_from_auth :active
     logcoll = collections(:real_log_collection)
     c, cr1 = minimal_new
     cr2 = ContainerRequest.new(DEFAULT_ATTRS)
@@ -698,6 +778,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "auth_uuid can set output, progress, runtime_status, state on running container -- but not log" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -718,6 +799,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "not allowed to set output that is not readable by current user" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -732,6 +814,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "other token cannot set output on running container" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -742,6 +825,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "can set trashed output on running container" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -755,6 +839,7 @@ class ContainerTest < ActiveSupport::TestCase
   end
 
   test "not allowed to set trashed output that is not readable by current user" do
+    set_user_from_auth :active
     c, _ = minimal_new
     set_user_from_auth :dispatch1
     c.lock
@@ -774,20 +859,24 @@ class ContainerTest < ActiveSupport::TestCase
     {state: Container::Complete, exit_code: 0, output: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'},
     {state: Container::Cancelled},
   ].each do |final_attrs|
-    test "secret_mounts is null after container is #{final_attrs[:state]}" do
+    test "secret_mounts and runtime_token are null after container is #{final_attrs[:state]}" do
+      set_user_from_auth :active
       c, cr = minimal_new(secret_mounts: {'/secret' => {'kind' => 'text', 'content' => 'foo'}},
-                          container_count_max: 1)
+                          container_count_max: 1, runtime_token: api_client_authorizations(:active).token)
       set_user_from_auth :dispatch1
       c.lock
       c.update_attributes!(state: Container::Running)
       c.reload
       assert c.secret_mounts.has_key?('/secret')
+      assert_equal api_client_authorizations(:active).token, c.runtime_token
 
       c.update_attributes!(final_attrs)
       c.reload
       assert_equal({}, c.secret_mounts)
+      assert_nil c.runtime_token
       cr.reload
       assert_equal({}, cr.secret_mounts)
+      assert_nil cr.runtime_token
       assert_no_secrets_logged
     end
   end
index be98a3ee113b7c30879f3039474534ea295f8b17..44560b80a0973e78488b2bf2dbb898877d98891f 100644 (file)
@@ -1432,7 +1432,7 @@ func (runner *ContainerRunner) ContainerToken() (string, error) {
        if err != nil {
                return "", err
        }
-       runner.token = auth.APIToken
+       runner.token = fmt.Sprintf("v2/%s/%s/%s", auth.UUID, auth.APIToken, runner.Container.UUID)
        return runner.token, nil
 }