Merge branch '17395-container-output-storage-class' into main
authorPeter Amstutz <peter.amstutz@curii.com>
Thu, 1 Jul 2021 20:13:58 +0000 (16:13 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Thu, 1 Jul 2021 20:13:58 +0000 (16:13 -0400)
refs #17395

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

13 files changed:
doc/admin/storage-classes.html.textile.liquid
doc/api/methods/container_requests.html.textile.liquid
doc/api/methods/containers.html.textile.liquid
lib/crunchrun/crunchrun.go
lib/crunchrun/crunchrun_test.go
sdk/go/arvados/container.go
sdk/go/keepclient/keepclient.go
services/api/app/models/container.rb
services/api/app/models/container_request.rb
services/api/db/migrate/20210621204455_add_container_output_storage_class.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/test/unit/container_request_test.rb
services/keepproxy/keepproxy.go

index e5c9a3973fa37226d4866eb5433fb64ae0a8b300..3e17831b58d6d32f21b0c278253aea4394334fac 100644 (file)
@@ -34,10 +34,10 @@ h3. Using storage classes
 
 h3. Storage management notes
 
-The "keep-balance":{{site.baseurl}}/install/install-keep-balance.html service is responsible for deciding which blocks should be placed on which keepstore volumes.  As part of the rebalancing behavior, it will determine where a block should go in order to satisfy the desired storage classes, and issue pull requests to copy the block from its original volume to the desired volume.  The block will subsequently be moved to trash on the original volume.
+When uploading data, if a data block cannot be uploaded to all desired storage classes, it will result in a fatal error.  Data blocks will not be uploaded to volumes that do not have the desired storage class.
 
-If a block appears in multiple collections with different storage classes, the block will be stored in separate volumes for each storage class, even if that results in overreplication, unless there is a volume which has all the desired storage classes.
+If you change the storage classes for a collection, the data is not moved immediately.  The "keep-balance":{{site.baseurl}}/install/install-keep-balance.html service is responsible for deciding which blocks should be placed on which keepstore volumes.  As part of the rebalancing behavior, it will determine where a block should go in order to satisfy the desired storage classes, and issue pull requests to copy the block from its original volume to the desired volume.  The block will subsequently be moved to trash on the original volume.
 
-If a collection has a desired storage class which is not available in any keepstore volume, the collection's blocks will remain in place, and an error will appear in the @keep-balance@ logs.
+If a block is assigned to multiple storage classes, the block will be stored on @desired_replication@ number of volumes for storage class, even if that results in overreplication.
 
-This feature does not provide a hard guarantee on where data will be stored.  Data may be written to default storage and moved to the desired storage class later.  If controlling data locality is a hard requirement (such as legal restrictions on the location of data) we recommend setting up multiple Arvados clusters.
+If a collection has a desired storage class which is not available in any keepstore volume, the collection's blocks will remain in place, and an error will appear in the @keep-balance@ logs.
index b24a24e0674b9a6f01e7463072e8ccd18ed15213..0aa96c3c38901c33af9c6ccbe6a983a518a470d5 100644 (file)
@@ -60,6 +60,7 @@ table(table table-bordered table-condensed).
 |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.||
+|output_storage_classes|array of strings|The storage classes that will be used for the log and output collections of this container request|default is ["default"]|
 
 h2(#priority). Priority
 
index 096a1fcaa9b628e6d5907ac33e8c6625f1114fd7..7da05cbd0b6812b68a136212a50b060fe1920476 100644 (file)
@@ -59,6 +59,7 @@ Generally this will contain additional keys that are not present in any correspo
 |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".|
 |gateway_address|string|Address (host:port) of gateway server.|Internal use only.|
 |interactive_session_started|boolean|Indicates whether @arvados-client shell@ has been used to run commands in the container, which may have altered the container's behavior and output.||
+|output_storage_classes|array of strings|The storage classes that will be used for the log and output collections of this container||
 
 h2(#container_states). Container states
 
index 5638e81e4de6670673dc2163577768323919d551..3c9c381619cfb757c25185d812b1c4bdf78f0f56 100644 (file)
@@ -60,6 +60,7 @@ type IKeepClient interface {
        ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error)
        LocalLocator(locator string) (string, error)
        ClearBlockCache()
+       SetStorageClasses(sc []string)
 }
 
 // NewLogWriter is a factory function to create a new log writer.
@@ -395,6 +396,7 @@ func (runner *ContainerRunner) SetupMounts() (map[string]bindmount, error) {
                "--foreground",
                "--allow-other",
                "--read-write",
+               "--storage-classes", strings.Join(runner.Container.OutputStorageClasses, ","),
                fmt.Sprintf("--crunchstat-interval=%v", runner.statInterval.Seconds())}
 
        if runner.Container.RuntimeConstraints.KeepCacheRAM > 0 {
@@ -1519,6 +1521,9 @@ func (runner *ContainerRunner) fetchContainerRecord() error {
                return fmt.Errorf("error creating container API client: %v", err)
        }
 
+       runner.ContainerKeepClient.SetStorageClasses(runner.Container.OutputStorageClasses)
+       runner.DispatcherKeepClient.SetStorageClasses(runner.Container.OutputStorageClasses)
+
        err = runner.ContainerArvClient.Call("GET", "containers", runner.Container.UUID, "secret_mounts", nil, &sm)
        if err != nil {
                if apierr, ok := err.(arvadosclient.APIServerError); !ok || apierr.HttpStatusCode != 404 {
index 5f7e71d95793e304c49dbd92ba5ac5fb9534ad93..4b1bf8425533e0aaaecdcf3229835edbfbaaef47 100644 (file)
@@ -39,11 +39,13 @@ func TestCrunchExec(t *testing.T) {
 var _ = Suite(&TestSuite{})
 
 type TestSuite struct {
-       client    *arvados.Client
-       api       *ArvTestClient
-       runner    *ContainerRunner
-       executor  *stubExecutor
-       keepmount string
+       client                   *arvados.Client
+       api                      *ArvTestClient
+       runner                   *ContainerRunner
+       executor                 *stubExecutor
+       keepmount                string
+       testDispatcherKeepClient KeepTestClient
+       testContainerKeepClient  KeepTestClient
 }
 
 func (s *TestSuite) SetUpTest(c *C) {
@@ -52,11 +54,11 @@ func (s *TestSuite) SetUpTest(c *C) {
        s.executor = &stubExecutor{}
        var err error
        s.api = &ArvTestClient{}
-       s.runner, err = NewContainerRunner(s.client, s.api, &KeepTestClient{}, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
+       s.runner, err = NewContainerRunner(s.client, s.api, &s.testDispatcherKeepClient, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
        c.Assert(err, IsNil)
        s.runner.executor = s.executor
        s.runner.MkArvClient = func(token string) (IArvadosClient, IKeepClient, *arvados.Client, error) {
-               return s.api, &KeepTestClient{}, s.client, nil
+               return s.api, &s.testContainerKeepClient, s.client, nil
        }
        s.runner.RunArvMount = func(cmd []string, tok string) (*exec.Cmd, error) {
                s.runner.ArvMountPoint = s.keepmount
@@ -88,8 +90,9 @@ type ArvTestClient struct {
 }
 
 type KeepTestClient struct {
-       Called  bool
-       Content []byte
+       Called         bool
+       Content        []byte
+       StorageClasses []string
 }
 
 type stubExecutor struct {
@@ -320,6 +323,10 @@ func (client *KeepTestClient) Close() {
        client.Content = nil
 }
 
+func (client *KeepTestClient) SetStorageClasses(sc []string) {
+       client.StorageClasses = sc
+}
+
 type FileWrapper struct {
        io.ReadCloser
        len int64
@@ -524,6 +531,7 @@ func (s *TestSuite) TestRunContainer(c *C) {
        s.runner.NewLogWriter = logs.NewTestLoggingWriter
        s.runner.Container.ContainerImage = arvadostest.DockerImage112PDH
        s.runner.Container.Command = []string{"./hw"}
+       s.runner.Container.OutputStorageClasses = []string{"default"}
 
        imageID, err := s.runner.LoadImage()
        c.Assert(err, IsNil)
@@ -654,7 +662,7 @@ func (s *TestSuite) fullRunHelper(c *C, record string, extraMounts []string, exi
                return d, err
        }
        s.runner.MkArvClient = func(token string) (IArvadosClient, IKeepClient, *arvados.Client, error) {
-               return &ArvTestClient{secretMounts: secretMounts}, &KeepTestClient{}, nil, nil
+               return &ArvTestClient{secretMounts: secretMounts}, &s.testContainerKeepClient, nil, nil
        }
 
        if extraMounts != nil && len(extraMounts) > 0 {
@@ -705,7 +713,8 @@ func (s *TestSuite) TestFullRunHello(c *C) {
     "output_path": "/tmp",
     "priority": 1,
     "runtime_constraints": {"vcpus":1,"ram":1000000},
-    "state": "Locked"
+    "state": "Locked",
+    "output_storage_classes": ["default"]
 }`, nil, 0, func() {
                c.Check(s.executor.created.Command, DeepEquals, []string{"echo", "hello world"})
                c.Check(s.executor.created.Image, Equals, "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678")
@@ -720,7 +729,8 @@ func (s *TestSuite) TestFullRunHello(c *C) {
        c.Check(s.api.CalledWith("container.exit_code", 0), NotNil)
        c.Check(s.api.CalledWith("container.state", "Complete"), NotNil)
        c.Check(s.api.Logs["stdout"].String(), Matches, ".*hello world\n")
-
+       c.Check(s.testDispatcherKeepClient.StorageClasses, DeepEquals, []string{"default"})
+       c.Check(s.testContainerKeepClient.StorageClasses, DeepEquals, []string{"default"})
 }
 
 func (s *TestSuite) TestRunAlreadyRunning(c *C) {
@@ -937,6 +947,29 @@ func (s *TestSuite) TestFullRunSetCwd(c *C) {
        c.Check(s.api.Logs["stdout"].String(), Matches, ".*/bin\n")
 }
 
+func (s *TestSuite) TestFullRunSetOutputStorageClasses(c *C) {
+       s.fullRunHelper(c, `{
+    "command": ["pwd"],
+    "container_image": "`+arvadostest.DockerImage112PDH+`",
+    "cwd": "/bin",
+    "environment": {},
+    "mounts": {"/tmp": {"kind": "tmp"} },
+    "output_path": "/tmp",
+    "priority": 1,
+    "runtime_constraints": {},
+    "state": "Locked",
+    "output_storage_classes": ["foo", "bar"]
+}`, nil, 0, func() {
+               fmt.Fprintln(s.executor.created.Stdout, s.executor.created.WorkingDir)
+       })
+
+       c.Check(s.api.CalledWith("container.exit_code", 0), NotNil)
+       c.Check(s.api.CalledWith("container.state", "Complete"), NotNil)
+       c.Check(s.api.Logs["stdout"].String(), Matches, ".*/bin\n")
+       c.Check(s.testDispatcherKeepClient.StorageClasses, DeepEquals, []string{"foo", "bar"})
+       c.Check(s.testContainerKeepClient.StorageClasses, DeepEquals, []string{"foo", "bar"})
+}
+
 func (s *TestSuite) TestStopOnSignal(c *C) {
        s.executor.runFunc = func() {
                s.executor.created.Stdout.Write([]byte("foo\n"))
@@ -1042,6 +1075,7 @@ func (s *TestSuite) TestSetupMounts(c *C) {
        cr.RunArvMount = am.ArvMountTest
        cr.ContainerArvClient = &ArvTestClient{}
        cr.ContainerKeepClient = &KeepTestClient{}
+       cr.Container.OutputStorageClasses = []string{"default"}
 
        realTemp := c.MkDir()
        certTemp := c.MkDir()
@@ -1079,7 +1113,7 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "default", "--crunchstat-interval=5",
                        "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{"/tmp": {realTemp + "/tmp2", false}})
                os.RemoveAll(cr.ArvMountPoint)
@@ -1094,11 +1128,12 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                cr.Container.Mounts["/out"] = arvados.Mount{Kind: "tmp"}
                cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
                cr.Container.OutputPath = "/out"
+               cr.Container.OutputStorageClasses = []string{"foo", "bar"}
 
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "foo,bar", "--crunchstat-interval=5",
                        "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{"/out": {realTemp + "/tmp2", false}, "/tmp": {realTemp + "/tmp3", false}})
                os.RemoveAll(cr.ArvMountPoint)
@@ -1113,11 +1148,12 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
                cr.Container.OutputPath = "/tmp"
                cr.Container.RuntimeConstraints.API = true
+               cr.Container.OutputStorageClasses = []string{"default"}
 
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "default", "--crunchstat-interval=5",
                        "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{"/tmp": {realTemp + "/tmp2", false}, "/etc/arvados/ca-certificates.crt": {stubCertPath, true}})
                os.RemoveAll(cr.ArvMountPoint)
@@ -1140,7 +1176,7 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "default", "--crunchstat-interval=5",
                        "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{"/keeptmp": {realTemp + "/keep1/tmp0", false}})
                os.RemoveAll(cr.ArvMountPoint)
@@ -1163,7 +1199,7 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "default", "--crunchstat-interval=5",
                        "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{
                        "/keepinp": {realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", true},
@@ -1190,7 +1226,7 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "default", "--crunchstat-interval=5",
                        "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{
                        "/keepinp": {realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", true},
@@ -1273,7 +1309,7 @@ func (s *TestSuite) TestSetupMounts(c *C) {
                bindmounts, err := cr.SetupMounts()
                c.Check(err, IsNil)
                c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
-                       "--read-write", "--crunchstat-interval=5",
+                       "--read-write", "--storage-classes", "default", "--crunchstat-interval=5",
                        "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
                c.Check(bindmounts, DeepEquals, map[string]bindmount{
                        "/tmp":     {realTemp + "/tmp2", false},
index f5426347086bcb4540be7834dbaa63317f33fc4c..b57dc849442f4934f10611acd0248539af3a827e 100644 (file)
@@ -32,6 +32,7 @@ type Container struct {
        FinishedAt                *time.Time             `json:"finished_at"` // nil if not yet finished
        GatewayAddress            string                 `json:"gateway_address"`
        InteractiveSessionStarted bool                   `json:"interactive_session_started"`
+       OutputStorageClasses      []string               `json:"output_storage_classes"`
 }
 
 // ContainerRequest is an arvados#container_request resource.
@@ -69,6 +70,7 @@ type ContainerRequest struct {
        ExpiresAt               time.Time              `json:"expires_at"`
        Filters                 []Filter               `json:"filters"`
        ContainerCount          int                    `json:"container_count"`
+       OutputStorageClasses    []string               `json:"output_storage_classes"`
 }
 
 // Mount is special behavior to attach to a filesystem path or device.
index 4541812651336096506b9a89da1f69b28ec3bd2a..2b560cff57b084786bca118f12609f00128d2623 100644 (file)
@@ -505,6 +505,11 @@ func (kc *KeepClient) ClearBlockCache() {
        kc.cache().Clear()
 }
 
+func (kc *KeepClient) SetStorageClasses(sc []string) {
+       // make a copy so the caller can't mess with it.
+       kc.StorageClasses = append([]string{}, sc...)
+}
+
 var (
        // There are four global http.Client objects for the four
        // possible permutations of TLS behavior (verify/skip-verify)
index e6d945a005c79dd3e3a30549bc43b4768aaed021..ddae4581892dd8f1bbe727ff0b67b04addb4c0a0 100644 (file)
@@ -22,6 +22,7 @@ class Container < ArvadosModel
   attribute :secret_mounts, :jsonbHash, default: {}
   attribute :runtime_status, :jsonbHash, default: {}
   attribute :runtime_auth_scopes, :jsonbHash, default: {}
+  attribute :output_storage_classes, :jsonbArray, default: ["default"]
 
   serialize :environment, Hash
   serialize :mounts, Hash
@@ -79,6 +80,7 @@ class Container < ArvadosModel
     t.add :lock_count
     t.add :gateway_address
     t.add :interactive_session_started
+    t.add :output_storage_classes
   end
 
   # Supported states for a container
@@ -104,11 +106,11 @@ class Container < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["secret_mounts", "secret_mounts_md5", "runtime_token", "gateway_address"]
+    super - ["secret_mounts", "secret_mounts_md5", "runtime_token", "gateway_address", "output_storage_classes"]
   end
 
   def self.searchable_columns *args
-    super - ["secret_mounts_md5", "runtime_token", "gateway_address"]
+    super - ["secret_mounts_md5", "runtime_token", "gateway_address", "output_storage_classes"]
   end
 
   def logged_attributes
@@ -187,7 +189,8 @@ class Container < ArvadosModel
         secret_mounts: req.secret_mounts,
         runtime_token: req.runtime_token,
         runtime_user_uuid: runtime_user.uuid,
-        runtime_auth_scopes: runtime_auth_scopes
+        runtime_auth_scopes: runtime_auth_scopes,
+        output_storage_classes: req.output_storage_classes,
       }
     end
     act_as_system_user do
@@ -467,7 +470,8 @@ class Container < ArvadosModel
                      :environment, :mounts, :output_path, :priority,
                      :runtime_constraints, :scheduling_parameters,
                      :secret_mounts, :runtime_token,
-                     :runtime_user_uuid, :runtime_auth_scopes)
+                     :runtime_user_uuid, :runtime_auth_scopes,
+                     :output_storage_classes)
     end
 
     case self.state
index e712acc6e9c37f85e5d9e40e7f5ec1990e0b947e..1de71102c61befff8e0aabef5885e7396405a237 100644 (file)
@@ -23,6 +23,7 @@ class ContainerRequest < ArvadosModel
   # already know how to properly treat them.
   attribute :properties, :jsonbHash, default: {}
   attribute :secret_mounts, :jsonbHash, default: {}
+  attribute :output_storage_classes, :jsonbArray, default: ["default"]
 
   serialize :environment, Hash
   serialize :mounts, Hash
@@ -76,6 +77,7 @@ class ContainerRequest < ArvadosModel
     t.add :scheduling_parameters
     t.add :state
     t.add :use_existing
+    t.add :output_storage_classes
   end
 
   # Supported states for a container request
@@ -97,7 +99,8 @@ class ContainerRequest < ArvadosModel
   :container_image, :cwd, :environment, :filters, :mounts,
   :output_path, :priority, :runtime_token,
   :runtime_constraints, :state, :container_uuid, :use_existing,
-  :scheduling_parameters, :secret_mounts, :output_name, :output_ttl]
+  :scheduling_parameters, :secret_mounts, :output_name, :output_ttl,
+  :output_storage_classes]
 
   def self.limit_index_columns_read
     ["mounts"]
@@ -177,7 +180,9 @@ class ContainerRequest < ArvadosModel
               'container_uuid' => container_uuid,
             },
             portable_data_hash: log_col.portable_data_hash,
-            manifest_text: log_col.manifest_text)
+            manifest_text: log_col.manifest_text,
+            storage_classes_desired: self.output_storage_classes
+          )
           completed_coll.save_with_unique_name!
         end
       end
@@ -211,6 +216,7 @@ class ContainerRequest < ArvadosModel
           owner_uuid: self.owner_uuid,
           name: coll_name,
           manifest_text: "",
+          storage_classes_desired: self.output_storage_classes,
           properties: {
             'type' => out_type,
             'container_request' => uuid,
@@ -237,7 +243,7 @@ class ContainerRequest < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["mounts", "secret_mounts", "secret_mounts_md5", "runtime_token"]
+    super - ["mounts", "secret_mounts", "secret_mounts_md5", "runtime_token", "output_storage_classes"]
   end
 
   protected
@@ -296,7 +302,8 @@ class ContainerRequest < ArvadosModel
         log_coll = Collection.new(
           owner_uuid: self.owner_uuid,
           name: coll_name = "Container log for request #{uuid}",
-          manifest_text: "")
+          manifest_text: "",
+          storage_classes_desired: self.output_storage_classes)
       end
 
       # copy logs from old container into CR's log collection
diff --git a/services/api/db/migrate/20210621204455_add_container_output_storage_class.rb b/services/api/db/migrate/20210621204455_add_container_output_storage_class.rb
new file mode 100644 (file)
index 0000000..93fe5fd
--- /dev/null
@@ -0,0 +1,10 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddContainerOutputStorageClass < ActiveRecord::Migration[5.2]
+  def change
+    add_column :container_requests, :output_storage_classes, :jsonb, :default => ["default"]
+    add_column :containers, :output_storage_classes, :jsonb, :default => ["default"]
+  end
+end
index 14eca609eb0e35c91215a2d70e5af898d90168d4..2bca887212a331143065d117816b81dc383f9b91 100644 (file)
@@ -238,6 +238,29 @@ SET default_tablespace = '';
 
 SET default_with_oids = false;
 
+--
+-- Name: groups; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.groups (
+    id integer NOT NULL,
+    uuid character varying(255),
+    owner_uuid character varying(255),
+    created_at timestamp without time zone NOT NULL,
+    modified_by_client_uuid character varying(255),
+    modified_by_user_uuid character varying(255),
+    modified_at timestamp without time zone,
+    name character varying(255) NOT NULL,
+    description character varying(524288),
+    updated_at timestamp without time zone NOT NULL,
+    group_class character varying(255),
+    trash_at timestamp without time zone,
+    is_trashed boolean DEFAULT false NOT NULL,
+    delete_at timestamp without time zone,
+    properties jsonb DEFAULT '{}'::jsonb
+);
+
+
 --
 -- Name: api_client_authorizations; Type: TABLE; Schema: public; Owner: -
 --
@@ -461,7 +484,8 @@ CREATE TABLE public.container_requests (
     output_name character varying(255) DEFAULT NULL::character varying,
     output_ttl integer DEFAULT 0 NOT NULL,
     secret_mounts jsonb DEFAULT '{}'::jsonb,
-    runtime_token text
+    runtime_token text,
+    output_storage_classes jsonb DEFAULT '["default"]'::jsonb
 );
 
 
@@ -523,7 +547,8 @@ CREATE TABLE public.containers (
     runtime_token text,
     lock_count integer DEFAULT 0 NOT NULL,
     gateway_address character varying,
-    interactive_session_started boolean DEFAULT false NOT NULL
+    interactive_session_started boolean DEFAULT false NOT NULL,
+    output_storage_classes jsonb DEFAULT '["default"]'::jsonb
 );
 
 
@@ -546,29 +571,6 @@ CREATE SEQUENCE public.containers_id_seq
 ALTER SEQUENCE public.containers_id_seq OWNED BY public.containers.id;
 
 
---
--- Name: groups; Type: TABLE; Schema: public; Owner: -
---
-
-CREATE TABLE public.groups (
-    id integer NOT NULL,
-    uuid character varying(255),
-    owner_uuid character varying(255),
-    created_at timestamp without time zone NOT NULL,
-    modified_by_client_uuid character varying(255),
-    modified_by_user_uuid character varying(255),
-    modified_at timestamp without time zone,
-    name character varying(255) NOT NULL,
-    description character varying(524288),
-    updated_at timestamp without time zone NOT NULL,
-    group_class character varying(255),
-    trash_at timestamp without time zone,
-    is_trashed boolean DEFAULT false NOT NULL,
-    delete_at timestamp without time zone,
-    properties jsonb DEFAULT '{}'::jsonb
-);
-
-
 --
 -- Name: groups_id_seq; Type: SEQUENCE; Schema: public; Owner: -
 --
@@ -3191,6 +3193,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20201105190435'),
 ('20201202174753'),
 ('20210108033940'),
-('20210126183521');
+('20210126183521'),
+('20210621204455');
 
 
index 2d5c73518191056a8b72909bc8d235a9d2f2ed39..7686e1a140618588fc15dcd8f71cb733d5af04c5 100644 (file)
@@ -1290,4 +1290,69 @@ class ContainerRequestTest < ActiveSupport::TestCase
       cr.save!
     end
   end
+
+  test "default output_storage_classes" do
+    act_as_user users(:active) do
+      cr = create_minimal_req!(priority: 1,
+                               state: ContainerRequest::Committed,
+                               output_name: 'foo')
+      run_container(cr)
+      cr.reload
+      output = Collection.find_by_uuid(cr.output_uuid)
+      assert_equal ["default"], output.storage_classes_desired
+    end
+  end
+
+  test "setting output_storage_classes" do
+    act_as_user users(:active) do
+      cr = create_minimal_req!(priority: 1,
+                               state: ContainerRequest::Committed,
+                               output_name: 'foo',
+                               output_storage_classes: ["foo_storage_class", "bar_storage_class"])
+      run_container(cr)
+      cr.reload
+      output = Collection.find_by_uuid(cr.output_uuid)
+      assert_equal ["foo_storage_class", "bar_storage_class"], output.storage_classes_desired
+      log = Collection.find_by_uuid(cr.log_uuid)
+      assert_equal ["foo_storage_class", "bar_storage_class"], log.storage_classes_desired
+    end
+  end
+
+  test "reusing container with different container_request.output_storage_classes" do
+    common_attrs = {cwd: "test",
+                    priority: 1,
+                    command: ["echo", "hello"],
+                    output_path: "test",
+                    runtime_constraints: {"vcpus" => 4,
+                                          "ram" => 12000000000},
+                    mounts: {"test" => {"kind" => "json"}},
+                    environment: {"var" => "value1"},
+                    output_storage_classes: ["foo_storage_class"]}
+    set_user_from_auth :active
+    cr1 = create_minimal_req!(common_attrs.merge({state: ContainerRequest::Committed}))
+    cont1 = run_container(cr1)
+    cr1.reload
+
+    output1 = Collection.find_by_uuid(cr1.output_uuid)
+
+    # Testing with use_existing default value
+    cr2 = create_minimal_req!(common_attrs.merge({state: ContainerRequest::Uncommitted,
+                                                  output_storage_classes: ["bar_storage_class"]}))
+
+    assert_not_nil cr1.container_uuid
+    assert_nil cr2.container_uuid
+
+    # Update cr2 to commited state, check for reuse, then run it
+    cr2.update_attributes!({state: ContainerRequest::Committed})
+    assert_equal cr1.container_uuid, cr2.container_uuid
+
+    cr2.reload
+    output2 = Collection.find_by_uuid(cr2.output_uuid)
+
+    # the original CR output has the original storage class,
+    # but the second CR output has the new storage class.
+    assert_equal ["foo_storage_class"], cont1.output_storage_classes
+    assert_equal ["foo_storage_class"], output1.storage_classes_desired
+    assert_equal ["bar_storage_class"], output2.storage_classes_desired
+  end
 end
index c679e5b91cff8f9b86daa0b8ccda60aa20b36f96..dd67aff797281829cd21da95e0400448775b9539 100644 (file)
@@ -531,7 +531,7 @@ func (h *proxyHandler) Put(resp http.ResponseWriter, req *http.Request) {
                for _, sc := range strings.Split(req.Header.Get("X-Keep-Storage-Classes"), ",") {
                        scl = append(scl, strings.Trim(sc, " "))
                }
-               kc.StorageClasses = scl
+               kc.SetStorageClasses(scl)
        }
 
        _, err = fmt.Sscanf(req.Header.Get("Content-Length"), "%d", &expectLength)