[ARVADOS] updated: 2.1.0-84-ge65d692c6

Git user git at public.arvados.org
Fri Feb 12 15:23:22 UTC 2021


Summary of changes:
 doc/admin/spot-instances.html.textile.liquid       |  23 +++--
 doc/api/keep-s3.html.textile.liquid                |   6 +-
 .../install-dispatch-cloud.html.textile.liquid     |   4 +
 doc/install/install-keep-web.html.textile.liquid   |   4 +-
 lib/cloud/azure/azure.go                           |  11 +++
 lib/cloud/azure/azure_test.go                      |  20 ++++
 lib/cloud/ec2/ec2.go                               |  21 +++--
 lib/config/config.default.yml                      |   2 +-
 lib/config/generated_config.go                     |   2 +-
 lib/dispatchcloud/node_size.go                     |   1 +
 lib/dispatchcloud/node_size_test.go                |   8 +-
 .../crunch-dispatch-local.service}                 |   8 +-
 .../fpm-info.sh}                                   |   3 +-
 .../crunch-dispatch-slurm/crunch-dispatch-slurm.go |   2 +-
 services/keep-web/handler.go                       |  15 +--
 services/keep-web/handler_test.go                  |  16 ++--
 services/keep-web/s3.go                            | 104 ++++++++++++++-------
 services/keep-web/s3_test.go                       | 101 ++++++++++++++++++--
 services/keep-web/server_test.go                   |  18 ++--
 services/keepstore/volume.go                       |   2 +-
 20 files changed, 284 insertions(+), 87 deletions(-)
 copy services/{crunch-dispatch-slurm/crunch-dispatch-slurm.service => crunch-dispatch-local/crunch-dispatch-local.service} (73%)
 copy services/{api/app/helpers/jobs_helper.rb => crunch-dispatch-local/fpm-info.sh} (78%)

       via  e65d692c62b5ef4963905114d096027059838573 (commit)
       via  4c963e053bc84b21d97bd3e1effa1c8903257a41 (commit)
       via  55f0317f1ae5e3b913a48a8c2b42f11c1953b9e3 (commit)
       via  6be4c965a7c2a77a6e6f39208d64a644efd08c18 (commit)
       via  b5e44060d19bcb6a039d25ec9c699fe1f9956631 (commit)
       via  714832393ce23e01066046056814bf4eb99b3cba (commit)
       via  5fa98bf78cc571d2362f9df7e5ad868f445144b4 (commit)
       via  7472f02bfad7b80ad009a3d25575749351e4801c (commit)
       via  c088aa0b2f17315c93a6ea254c962b726df4b47e (commit)
       via  5b5a5ad9bd96cccda87576c39e61cda3bcd57daa (commit)
       via  2dab77693f534331324293aac49a36dc43119186 (commit)
       via  e0fe85baa8500dc8eca281982eb92c1612046723 (commit)
       via  41b689b00a32e1f2a11e32125c7710bdc0887d99 (commit)
       via  7da491928d7444147f4d864d5c101577e7f725e5 (commit)
       via  acf4fc32585cef9032b400abaa54582aed0ac76b (commit)
       via  cc22d5357dd0ca74036b52a50ea06597ee1c719f (commit)
       via  cffdc810f4cf76eba807acbd5dd0539b1c77480a (commit)
      from  00a00c5173675c41c5096f953f3b4095309c54da (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit e65d692c62b5ef4963905114d096027059838573
Author: Ward Vandewege <ward at curii.com>
Date:   Tue Feb 9 15:14:10 2021 -0500

    17355: keepstore should take the volume's AccessViaHosts ReadOnly flag
           into account when reporting ReadOnly status of a volume on
           startup.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/services/keepstore/volume.go b/services/keepstore/volume.go
index 4d8a0aec7..26e6b7318 100644
--- a/services/keepstore/volume.go
+++ b/services/keepstore/volume.go
@@ -315,7 +315,7 @@ func makeRRVolumeManager(logger logrus.FieldLogger, cluster *arvados.Cluster, my
 		if err != nil {
 			return nil, fmt.Errorf("error initializing volume %s: %s", uuid, err)
 		}
-		logger.Printf("started volume %s (%s), ReadOnly=%v", uuid, vol, cfgvol.ReadOnly)
+		logger.Printf("started volume %s (%s), ReadOnly=%v", uuid, vol, cfgvol.ReadOnly || va.ReadOnly)
 
 		sc := cfgvol.StorageClasses
 		if len(sc) == 0 {

commit 4c963e053bc84b21d97bd3e1effa1c8903257a41
Author: Ward Vandewege <ward at curii.com>
Date:   Thu Feb 4 14:07:10 2021 -0500

    17340: arvados-dispatch-cloud should take the Containers.ReserveExtraRAM
           configuration parameter into account when choosing a node size.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/lib/dispatchcloud/node_size.go b/lib/dispatchcloud/node_size.go
index fd0486086..7e8ce0bf4 100644
--- a/lib/dispatchcloud/node_size.go
+++ b/lib/dispatchcloud/node_size.go
@@ -96,6 +96,7 @@ func ChooseInstanceType(cc *arvados.Cluster, ctr *arvados.Container) (best arvad
 	needVCPUs := ctr.RuntimeConstraints.VCPUs
 
 	needRAM := ctr.RuntimeConstraints.RAM + ctr.RuntimeConstraints.KeepCacheRAM
+	needRAM += int64(cc.Containers.ReserveExtraRAM)
 	needRAM = (needRAM * 100) / int64(100-discountConfiguredRAMPercent)
 
 	ok := false
diff --git a/lib/dispatchcloud/node_size_test.go b/lib/dispatchcloud/node_size_test.go
index ea98efe1d..abd292cba 100644
--- a/lib/dispatchcloud/node_size_test.go
+++ b/lib/dispatchcloud/node_size_test.go
@@ -73,8 +73,14 @@ func (*NodeSizeSuite) TestChoose(c *check.C) {
 			"best":   {Price: 3.3, RAM: 4000000000, VCPUs: 4, Scratch: 2 * GiB, Name: "best"},
 			"costly": {Price: 4.4, RAM: 4000000000, VCPUs: 8, Scratch: 2 * GiB, Name: "costly"},
 		},
+		{
+			"small":  {Price: 1.1, RAM: 1000000000, VCPUs: 2, Scratch: GiB, Name: "small"},
+			"nearly": {Price: 2.2, RAM: 1200000000, VCPUs: 4, Scratch: 2 * GiB, Name: "nearly"},
+			"best":   {Price: 3.3, RAM: 4000000000, VCPUs: 4, Scratch: 2 * GiB, Name: "best"},
+			"costly": {Price: 4.4, RAM: 4000000000, VCPUs: 8, Scratch: 2 * GiB, Name: "costly"},
+		},
 	} {
-		best, err := ChooseInstanceType(&arvados.Cluster{InstanceTypes: menu}, &arvados.Container{
+		best, err := ChooseInstanceType(&arvados.Cluster{InstanceTypes: menu, Containers: arvados.ContainersConfig{ReserveExtraRAM: 268435456}}, &arvados.Container{
 			Mounts: map[string]arvados.Mount{
 				"/tmp": {Kind: "tmp", Capacity: 2 * int64(GiB)},
 			},

commit 55f0317f1ae5e3b913a48a8c2b42f11c1953b9e3
Author: Javier Bértoli <jbertoli at curii.com>
Date:   Mon Oct 26 12:28:29 2020 -0300

    feat(crunch-dispatch-local): add crunch-run dependency
    
    refs #16995
    Arvados-DCO-1.1-Signed-off-by: Javier Bértoli <jbertoli at curii.com>

diff --git a/services/crunch-dispatch-local/fpm-info.sh b/services/crunch-dispatch-local/fpm-info.sh
new file mode 100644
index 000000000..6956c4c59
--- /dev/null
+++ b/services/crunch-dispatch-local/fpm-info.sh
@@ -0,0 +1,5 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+fpm_depends+=(crunch-run)

commit 6be4c965a7c2a77a6e6f39208d64a644efd08c18
Author: Javier Bértoli <jbertoli at curii.com>
Date:   Mon Oct 19 10:06:18 2020 -0300

    fix(crunch-dispatch-local): add missing service file
    
    refs #16996
    Arvados-DCO-1.1-Signed-off-by: Javier Bértoli <jbertoli at curii.com>

diff --git a/services/crunch-dispatch-local/crunch-dispatch-local.service b/services/crunch-dispatch-local/crunch-dispatch-local.service
new file mode 100644
index 000000000..692d81e57
--- /dev/null
+++ b/services/crunch-dispatch-local/crunch-dispatch-local.service
@@ -0,0 +1,29 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+[Unit]
+Description=Arvados Crunch Dispatcher for LOCAL service
+Documentation=https://doc.arvados.org/
+After=network.target
+
+# systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
+StartLimitInterval=0
+
+# systemd>=230 (debian:9) obeys StartLimitIntervalSec in the [Unit] section
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+EnvironmentFile=-/etc/arvados/crunch-dispatch-local-credentials
+ExecStart=/usr/bin/crunch-dispatch-local -poll-interval=1 -crunch-run-command=/usr/bin/crunch-run
+# Set a reasonable default for the open file limit
+LimitNOFILE=65536
+Restart=always
+RestartSec=1
+LimitNOFILE=1000000
+
+# systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target

commit b5e44060d19bcb6a039d25ec9c699fe1f9956631
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Tue Nov 24 13:40:21 2020 -0500

    16774: text/plain response uses crlf.  Tests check error codes.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/services/keep-web/handler.go b/services/keep-web/handler.go
index f2e9ba5aa..ab1bc080b 100644
--- a/services/keep-web/handler.go
+++ b/services/keep-web/handler.go
@@ -62,8 +62,8 @@ func parseCollectionIDFromDNSName(s string) string {
 
 var urlPDHDecoder = strings.NewReplacer(" ", "+", "-", "+")
 
-var notFoundMessage = "404 Not found\n\nThe requested path was not found, or you do not have permission to access it."
-var unauthorizedMessage = "401 Unauthorized\n\nA valid Arvados token must be provided to access this resource."
+var notFoundMessage = "404 Not found\r\n\r\nThe requested path was not found, or you do not have permission to access it.\r"
+var unauthorizedMessage = "401 Unauthorized\r\n\r\nA valid Arvados token must be provided to access this resource.\r"
 
 // parseCollectionIDFromURL returns a UUID or PDH if s is a UUID or a
 // PDH (even if it is a PDH with "+" replaced by " " or "-");
diff --git a/services/keep-web/s3_test.go b/services/keep-web/s3_test.go
index 78b94e20a..6a7eeef37 100644
--- a/services/keep-web/s3_test.go
+++ b/services/keep-web/s3_test.go
@@ -175,6 +175,8 @@ func (s *IntegrationSuite) testS3GetObject(c *check.C, bucket *s3.Bucket, prefix
 
 	// GetObject
 	rdr, err = bucket.GetReader(prefix + "missingfile")
+	c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
+	c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
 	c.Check(err, check.ErrorMatches, `The specified key does not exist.`)
 
 	// HeadObject
@@ -237,6 +239,8 @@ func (s *IntegrationSuite) testS3PutObjectSuccess(c *check.C, bucket *s3.Bucket,
 		objname := prefix + trial.path
 
 		_, err := bucket.GetReader(objname)
+		c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
+		c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
 		c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
 
 		buf := make([]byte, trial.size)
@@ -286,15 +290,21 @@ func (s *IntegrationSuite) TestS3ProjectPutObjectNotSupported(c *check.C) {
 		c.Logf("=== %v", trial)
 
 		_, err := bucket.GetReader(trial.path)
+		c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
+		c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
 		c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
 
 		buf := make([]byte, trial.size)
 		rand.Read(buf)
 
 		err = bucket.PutReader(trial.path, bytes.NewReader(buf), int64(len(buf)), trial.contentType, s3.Private, s3.Options{})
+		c.Check(err.(*s3.Error).StatusCode, check.Equals, 400)
+		c.Check(err.(*s3.Error).Code, check.Equals, `InvalidArgument`)
 		c.Check(err, check.ErrorMatches, `(mkdir "by_id/zzzzz-j7d0g-[a-z0-9]{15}/newdir2?"|open "/zzzzz-j7d0g-[a-z0-9]{15}/newfile") failed: invalid argument`)
 
 		_, err = bucket.GetReader(trial.path)
+		c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
+		c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
 		c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
 	}
 }
@@ -403,6 +413,8 @@ func (s *IntegrationSuite) testS3PutObjectFailure(c *check.C, bucket *s3.Bucket,
 
 			if objname != "" && objname != "/" {
 				_, err = bucket.GetReader(objname)
+				c.Check(err.(*s3.Error).StatusCode, check.Equals, 404)
+				c.Check(err.(*s3.Error).Code, check.Equals, `NoSuchKey`)
 				c.Check(err, check.ErrorMatches, `The specified key does not exist.`, check.Commentf("GET %q should return 404", objname))
 			}
 		}()

commit 714832393ce23e01066046056814bf4eb99b3cba
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Mon Nov 23 15:04:48 2020 -0500

    16774: Fix tests.  Use encoder for xml error response.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/services/keep-web/handler_test.go b/services/keep-web/handler_test.go
index f6f3de887..8e2e05c76 100644
--- a/services/keep-web/handler_test.go
+++ b/services/keep-web/handler_test.go
@@ -122,7 +122,7 @@ func (s *IntegrationSuite) TestVhost404(c *check.C) {
 		}
 		s.testServer.Handler.ServeHTTP(resp, req)
 		c.Check(resp.Code, check.Equals, http.StatusNotFound)
-		c.Check(resp.Body.String(), check.Equals, "")
+		c.Check(resp.Body.String(), check.Equals, notFoundMessage+"\n")
 	}
 }
 
@@ -250,7 +250,11 @@ func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authori
 				// depending on the authz method.
 				c.Check(code, check.Equals, failCode)
 			}
-			c.Check(body, check.Equals, "")
+			if code == 404 {
+				c.Check(body, check.Equals, notFoundMessage+"\n")
+			} else {
+				c.Check(body, check.Equals, unauthorizedMessage+"\n")
+			}
 		}
 	}
 }
@@ -307,7 +311,7 @@ func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
 		"",
 		"",
 		http.StatusNotFound,
-		"",
+		notFoundMessage+"\n",
 	)
 }
 
@@ -321,7 +325,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C)
 		"",
 		"",
 		http.StatusUnauthorized,
-		"",
+		unauthorizedMessage+"\n",
 	)
 }
 
@@ -439,7 +443,7 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C)
 		"application/x-www-form-urlencoded",
 		url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
 		http.StatusNotFound,
-		"",
+		notFoundMessage+"\n",
 	)
 }
 
@@ -463,7 +467,7 @@ func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
 		"",
 		"",
 		http.StatusNotFound,
-		"",
+		notFoundMessage+"\n",
 	)
 }
 
diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go
index 8655704d3..a6dfa9998 100644
--- a/services/keep-web/s3.go
+++ b/services/keep-web/s3.go
@@ -177,13 +177,19 @@ func s3ErrorResponse(w http.ResponseWriter, s3code string, message string, resou
 	w.Header().Set("Content-Type", "application/xml")
 	w.Header().Set("X-Content-Type-Options", "nosniff")
 	w.WriteHeader(code)
-	fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?>
-<Error>
-  <Code>%v</Code>
-  <Message>%v</Message>
-  <Resource>%v</Resource>
-  <RequestId></RequestId>
-</Error>`, code, message, resource)
+	var errstruct struct {
+		Code      string
+		Message   string
+		Resource  string
+		RequestId string
+	}
+	errstruct.Code = s3code
+	errstruct.Message = message
+	errstruct.Resource = resource
+	errstruct.RequestId = ""
+	enc := xml.NewEncoder(w)
+	fmt.Fprint(w, xml.Header)
+	enc.EncodeElement(errstruct, xml.StartElement{Name: xml.Name{Local: "Error"}})
 }
 
 var NoSuchKey = "NoSuchKey"
diff --git a/services/keep-web/s3_test.go b/services/keep-web/s3_test.go
index 07103d107..78b94e20a 100644
--- a/services/keep-web/s3_test.go
+++ b/services/keep-web/s3_test.go
@@ -175,7 +175,7 @@ func (s *IntegrationSuite) testS3GetObject(c *check.C, bucket *s3.Bucket, prefix
 
 	// GetObject
 	rdr, err = bucket.GetReader(prefix + "missingfile")
-	c.Check(err, check.ErrorMatches, `404 Not Found`)
+	c.Check(err, check.ErrorMatches, `The specified key does not exist.`)
 
 	// HeadObject
 	exists, err := bucket.Exists(prefix + "missingfile")
@@ -237,7 +237,7 @@ func (s *IntegrationSuite) testS3PutObjectSuccess(c *check.C, bucket *s3.Bucket,
 		objname := prefix + trial.path
 
 		_, err := bucket.GetReader(objname)
-		c.Assert(err, check.ErrorMatches, `404 Not Found`)
+		c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
 
 		buf := make([]byte, trial.size)
 		rand.Read(buf)
@@ -286,16 +286,16 @@ func (s *IntegrationSuite) TestS3ProjectPutObjectNotSupported(c *check.C) {
 		c.Logf("=== %v", trial)
 
 		_, err := bucket.GetReader(trial.path)
-		c.Assert(err, check.ErrorMatches, `404 Not Found`)
+		c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
 
 		buf := make([]byte, trial.size)
 		rand.Read(buf)
 
 		err = bucket.PutReader(trial.path, bytes.NewReader(buf), int64(len(buf)), trial.contentType, s3.Private, s3.Options{})
-		c.Check(err, check.ErrorMatches, `400 Bad Request`)
+		c.Check(err, check.ErrorMatches, `(mkdir "by_id/zzzzz-j7d0g-[a-z0-9]{15}/newdir2?"|open "/zzzzz-j7d0g-[a-z0-9]{15}/newfile") failed: invalid argument`)
 
 		_, err = bucket.GetReader(trial.path)
-		c.Assert(err, check.ErrorMatches, `404 Not Found`)
+		c.Assert(err, check.ErrorMatches, `The specified key does not exist.`)
 	}
 }
 
@@ -397,13 +397,13 @@ func (s *IntegrationSuite) testS3PutObjectFailure(c *check.C, bucket *s3.Bucket,
 			rand.Read(buf)
 
 			err := bucket.PutReader(objname, bytes.NewReader(buf), int64(len(buf)), "application/octet-stream", s3.Private, s3.Options{})
-			if !c.Check(err, check.ErrorMatches, `400 Bad.*`, check.Commentf("PUT %q should fail", objname)) {
+			if !c.Check(err, check.ErrorMatches, `(invalid object name.*|open ".*" failed.*|object name conflicts with existing object|Missing object name in PUT request.)`, check.Commentf("PUT %q should fail", objname)) {
 				return
 			}
 
 			if objname != "" && objname != "/" {
 				_, err = bucket.GetReader(objname)
-				c.Check(err, check.ErrorMatches, `404 Not Found`, check.Commentf("GET %q should return 404", objname))
+				c.Check(err, check.ErrorMatches, `The specified key does not exist.`, check.Commentf("GET %q should return 404", objname))
 			}
 		}()
 	}
diff --git a/services/keep-web/server_test.go b/services/keep-web/server_test.go
index 43817b51f..0a1c7d1b3 100644
--- a/services/keep-web/server_test.go
+++ b/services/keep-web/server_test.go
@@ -43,17 +43,17 @@ func (s *IntegrationSuite) TestNoToken(c *check.C) {
 	} {
 		hdr, body, _ := s.runCurl(c, token, "collections.example.com", "/collections/"+arvadostest.FooCollection+"/foo")
 		c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`)
-		c.Check(body, check.Equals, "")
+		c.Check(body, check.Equals, notFoundMessage+"\n")
 
 		if token != "" {
 			hdr, body, _ = s.runCurl(c, token, "collections.example.com", "/collections/download/"+arvadostest.FooCollection+"/"+token+"/foo")
 			c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`)
-			c.Check(body, check.Equals, "")
+			c.Check(body, check.Equals, notFoundMessage+"\n")
 		}
 
 		hdr, body, _ = s.runCurl(c, token, "collections.example.com", "/bad-route")
 		c.Check(hdr, check.Matches, `(?s)HTTP/1.1 404 Not Found\r\n.*`)
-		c.Check(body, check.Equals, "")
+		c.Check(body, check.Equals, notFoundMessage+"\n")
 	}
 }
 
@@ -86,7 +86,7 @@ func (s *IntegrationSuite) Test404(c *check.C) {
 		hdr, body, _ := s.runCurl(c, arvadostest.ActiveToken, "collections.example.com", uri)
 		c.Check(hdr, check.Matches, "(?s)HTTP/1.1 404 Not Found\r\n.*")
 		if len(body) > 0 {
-			c.Check(body, check.Equals, "404 page not found\n")
+			c.Check(body, check.Equals, notFoundMessage+"\n")
 		}
 	}
 }

commit 5fa98bf78cc571d2362f9df7e5ad868f445144b4
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Nov 20 18:03:30 2020 -0500

    16774: Keep-web errors include messages
    
    Errors on the regular keep-web side return plain text responses.
    
    Errors on the S3 side return XML error responses that S3 clients
    expect.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/services/keep-web/handler.go b/services/keep-web/handler.go
index 963948cc6..f2e9ba5aa 100644
--- a/services/keep-web/handler.go
+++ b/services/keep-web/handler.go
@@ -62,6 +62,9 @@ func parseCollectionIDFromDNSName(s string) string {
 
 var urlPDHDecoder = strings.NewReplacer(" ", "+", "-", "+")
 
+var notFoundMessage = "404 Not found\n\nThe requested path was not found, or you do not have permission to access it."
+var unauthorizedMessage = "401 Unauthorized\n\nA valid Arvados token must be provided to access this resource."
+
 // parseCollectionIDFromURL returns a UUID or PDH if s is a UUID or a
 // PDH (even if it is a PDH with "+" replaced by " " or "-");
 // otherwise "".
@@ -279,7 +282,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	}
 
 	if collectionID == "" && !useSiteFS {
-		w.WriteHeader(http.StatusNotFound)
+		http.Error(w, notFoundMessage, http.StatusNotFound)
 		return
 	}
 
@@ -388,14 +391,14 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 			// for additional credentials would just be
 			// confusing), or we don't even accept
 			// credentials at this path.
-			w.WriteHeader(http.StatusNotFound)
+			http.Error(w, notFoundMessage, http.StatusNotFound)
 			return
 		}
 		for _, t := range reqTokens {
 			if tokenResult[t] == 404 {
 				// The client provided valid token(s), but the
 				// collection was not found.
-				w.WriteHeader(http.StatusNotFound)
+				http.Error(w, notFoundMessage, http.StatusNotFound)
 				return
 			}
 		}
@@ -409,7 +412,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 		// data that has been deleted.  Allow a referrer to
 		// provide this context somehow?
 		w.Header().Add("WWW-Authenticate", "Basic realm=\"collections\"")
-		w.WriteHeader(http.StatusUnauthorized)
+		http.Error(w, unauthorizedMessage, http.StatusUnauthorized)
 		return
 	}
 
@@ -479,7 +482,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	openPath := "/" + strings.Join(targetPath, "/")
 	if f, err := fs.Open(openPath); os.IsNotExist(err) {
 		// Requested non-existent path
-		w.WriteHeader(http.StatusNotFound)
+		http.Error(w, notFoundMessage, http.StatusNotFound)
 	} else if err != nil {
 		// Some other (unexpected) error
 		http.Error(w, "open: "+err.Error(), http.StatusInternalServerError)
@@ -533,7 +536,7 @@ func (h *handler) getClients(reqID, token string) (arv *arvadosclient.ArvadosCli
 func (h *handler) serveSiteFS(w http.ResponseWriter, r *http.Request, tokens []string, credentialsOK, attachment bool) {
 	if len(tokens) == 0 {
 		w.Header().Add("WWW-Authenticate", "Basic realm=\"collections\"")
-		http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+		http.Error(w, unauthorizedMessage, http.StatusUnauthorized)
 		return
 	}
 	if writeMethod[r.Method] {
diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go
index bcd1b55b2..8655704d3 100644
--- a/services/keep-web/s3.go
+++ b/services/keep-web/s3.go
@@ -173,6 +173,27 @@ func (h *handler) checks3signature(r *http.Request) (string, error) {
 	return secret, nil
 }
 
+func s3ErrorResponse(w http.ResponseWriter, s3code string, message string, resource string, code int) {
+	w.Header().Set("Content-Type", "application/xml")
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.WriteHeader(code)
+	fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?>
+<Error>
+  <Code>%v</Code>
+  <Message>%v</Message>
+  <Resource>%v</Resource>
+  <RequestId></RequestId>
+</Error>`, code, message, resource)
+}
+
+var NoSuchKey = "NoSuchKey"
+var NoSuchBucket = "NoSuchBucket"
+var InvalidArgument = "InvalidArgument"
+var InternalError = "InternalError"
+var UnauthorizedAccess = "UnauthorizedAccess"
+var InvalidRequest = "InvalidRequest"
+var SignatureDoesNotMatch = "SignatureDoesNotMatch"
+
 // serveS3 handles r and returns true if r is a request from an S3
 // client, otherwise it returns false.
 func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
@@ -180,14 +201,14 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 	if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "AWS ") {
 		split := strings.SplitN(auth[4:], ":", 2)
 		if len(split) < 2 {
-			http.Error(w, "malformed Authorization header", http.StatusUnauthorized)
+			s3ErrorResponse(w, InvalidRequest, "malformed Authorization header", r.URL.Path, http.StatusUnauthorized)
 			return true
 		}
 		token = split[0]
 	} else if strings.HasPrefix(auth, s3SignAlgorithm+" ") {
 		t, err := h.checks3signature(r)
 		if err != nil {
-			http.Error(w, "signature verification failed: "+err.Error(), http.StatusForbidden)
+			s3ErrorResponse(w, SignatureDoesNotMatch, "signature verification failed: "+err.Error(), r.URL.Path, http.StatusForbidden)
 			return true
 		}
 		token = t
@@ -197,7 +218,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 
 	_, kc, client, release, err := h.getClients(r.Header.Get("X-Request-Id"), token)
 	if err != nil {
-		http.Error(w, "Pool failed: "+h.clientPool.Err().Error(), http.StatusInternalServerError)
+		s3ErrorResponse(w, InternalError, "Pool failed: "+h.clientPool.Err().Error(), r.URL.Path, http.StatusInternalServerError)
 		return true
 	}
 	defer release()
@@ -238,9 +259,9 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 			if err == nil && fi.IsDir() {
 				w.WriteHeader(http.StatusOK)
 			} else if os.IsNotExist(err) {
-				w.WriteHeader(http.StatusNotFound)
+				s3ErrorResponse(w, NoSuchBucket, "The specified bucket does not exist.", r.URL.Path, http.StatusNotFound)
 			} else {
-				http.Error(w, err.Error(), http.StatusBadGateway)
+				s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway)
 			}
 			return true
 		}
@@ -252,7 +273,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 		if os.IsNotExist(err) ||
 			(err != nil && err.Error() == "not a directory") ||
 			(fi != nil && fi.IsDir()) {
-			http.Error(w, "not found", http.StatusNotFound)
+			s3ErrorResponse(w, NoSuchKey, "The specified key does not exist.", r.URL.Path, http.StatusNotFound)
 			return true
 		}
 		// shallow copy r, and change URL path
@@ -262,24 +283,24 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 		return true
 	case r.Method == http.MethodPut:
 		if !objectNameGiven {
-			http.Error(w, "missing object name in PUT request", http.StatusBadRequest)
+			s3ErrorResponse(w, InvalidArgument, "Missing object name in PUT request.", r.URL.Path, http.StatusBadRequest)
 			return true
 		}
 		var objectIsDir bool
 		if strings.HasSuffix(fspath, "/") {
 			if !h.Config.cluster.Collections.S3FolderObjects {
-				http.Error(w, "invalid object name: trailing slash", http.StatusBadRequest)
+				s3ErrorResponse(w, InvalidArgument, "invalid object name: trailing slash", r.URL.Path, http.StatusBadRequest)
 				return true
 			}
 			n, err := r.Body.Read(make([]byte, 1))
 			if err != nil && err != io.EOF {
-				http.Error(w, fmt.Sprintf("error reading request body: %s", err), http.StatusInternalServerError)
+				s3ErrorResponse(w, InternalError, fmt.Sprintf("error reading request body: %s", err), r.URL.Path, http.StatusInternalServerError)
 				return true
 			} else if n > 0 {
-				http.Error(w, "cannot create object with trailing '/' char unless content is empty", http.StatusBadRequest)
+				s3ErrorResponse(w, InvalidArgument, "cannot create object with trailing '/' char unless content is empty", r.URL.Path, http.StatusBadRequest)
 				return true
 			} else if strings.SplitN(r.Header.Get("Content-Type"), ";", 2)[0] != "application/x-directory" {
-				http.Error(w, "cannot create object with trailing '/' char unless Content-Type is 'application/x-directory'", http.StatusBadRequest)
+				s3ErrorResponse(w, InvalidArgument, "cannot create object with trailing '/' char unless Content-Type is 'application/x-directory'", r.URL.Path, http.StatusBadRequest)
 				return true
 			}
 			// Given PUT "foo/bar/", we'll use "foo/bar/."
@@ -291,12 +312,12 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 		fi, err := fs.Stat(fspath)
 		if err != nil && err.Error() == "not a directory" {
 			// requested foo/bar, but foo is a file
-			http.Error(w, "object name conflicts with existing object", http.StatusBadRequest)
+			s3ErrorResponse(w, InvalidArgument, "object name conflicts with existing object", r.URL.Path, http.StatusBadRequest)
 			return true
 		}
 		if strings.HasSuffix(r.URL.Path, "/") && err == nil && !fi.IsDir() {
 			// requested foo/bar/, but foo/bar is a file
-			http.Error(w, "object name conflicts with existing object", http.StatusBadRequest)
+			s3ErrorResponse(w, InvalidArgument, "object name conflicts with existing object", r.URL.Path, http.StatusBadRequest)
 			return true
 		}
 		// create missing parent/intermediate directories, if any
@@ -305,7 +326,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 				dir := fspath[:i]
 				if strings.HasSuffix(dir, "/") {
 					err = errors.New("invalid object name (consecutive '/' chars)")
-					http.Error(w, err.Error(), http.StatusBadRequest)
+					s3ErrorResponse(w, InvalidArgument, err.Error(), r.URL.Path, http.StatusBadRequest)
 					return true
 				}
 				err = fs.Mkdir(dir, 0755)
@@ -313,11 +334,11 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 					// Cannot create a directory
 					// here.
 					err = fmt.Errorf("mkdir %q failed: %w", dir, err)
-					http.Error(w, err.Error(), http.StatusBadRequest)
+					s3ErrorResponse(w, InvalidArgument, err.Error(), r.URL.Path, http.StatusBadRequest)
 					return true
 				} else if err != nil && !os.IsExist(err) {
 					err = fmt.Errorf("mkdir %q failed: %w", dir, err)
-					http.Error(w, err.Error(), http.StatusInternalServerError)
+					s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusInternalServerError)
 					return true
 				}
 			}
@@ -329,34 +350,34 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 			}
 			if err != nil {
 				err = fmt.Errorf("open %q failed: %w", r.URL.Path, err)
-				http.Error(w, err.Error(), http.StatusBadRequest)
+				s3ErrorResponse(w, InvalidArgument, err.Error(), r.URL.Path, http.StatusBadRequest)
 				return true
 			}
 			defer f.Close()
 			_, err = io.Copy(f, r.Body)
 			if err != nil {
 				err = fmt.Errorf("write to %q failed: %w", r.URL.Path, err)
-				http.Error(w, err.Error(), http.StatusBadGateway)
+				s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway)
 				return true
 			}
 			err = f.Close()
 			if err != nil {
 				err = fmt.Errorf("write to %q failed: close: %w", r.URL.Path, err)
-				http.Error(w, err.Error(), http.StatusBadGateway)
+				s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway)
 				return true
 			}
 		}
 		err = fs.Sync()
 		if err != nil {
 			err = fmt.Errorf("sync failed: %w", err)
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusInternalServerError)
 			return true
 		}
 		w.WriteHeader(http.StatusOK)
 		return true
 	case r.Method == http.MethodDelete:
 		if !objectNameGiven || r.URL.Path == "/" {
-			http.Error(w, "missing object name in DELETE request", http.StatusBadRequest)
+			s3ErrorResponse(w, InvalidArgument, "missing object name in DELETE request", r.URL.Path, http.StatusBadRequest)
 			return true
 		}
 		if strings.HasSuffix(fspath, "/") {
@@ -366,7 +387,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 				w.WriteHeader(http.StatusNoContent)
 				return true
 			} else if err != nil {
-				http.Error(w, err.Error(), http.StatusInternalServerError)
+				s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusInternalServerError)
 				return true
 			} else if !fi.IsDir() {
 				// if "foo" exists and is a file, then
@@ -390,19 +411,20 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 		}
 		if err != nil {
 			err = fmt.Errorf("rm failed: %w", err)
-			http.Error(w, err.Error(), http.StatusBadRequest)
+			s3ErrorResponse(w, InvalidArgument, err.Error(), r.URL.Path, http.StatusBadRequest)
 			return true
 		}
 		err = fs.Sync()
 		if err != nil {
 			err = fmt.Errorf("sync failed: %w", err)
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusInternalServerError)
 			return true
 		}
 		w.WriteHeader(http.StatusNoContent)
 		return true
 	default:
-		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		s3ErrorResponse(w, InvalidRequest, "method not allowed", r.URL.Path, http.StatusMethodNotAllowed)
+
 		return true
 	}
 }

commit 7472f02bfad7b80ad009a3d25575749351e4801c
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Nov 19 15:36:29 2020 -0500

    17015: Ensure that containers belong to this cluster
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/services/crunch-dispatch-slurm/crunch-dispatch-slurm.go b/services/crunch-dispatch-slurm/crunch-dispatch-slurm.go
index 4115482d8..a5899ce8a 100644
--- a/services/crunch-dispatch-slurm/crunch-dispatch-slurm.go
+++ b/services/crunch-dispatch-slurm/crunch-dispatch-slurm.go
@@ -202,7 +202,7 @@ var containerUuidPattern = regexp.MustCompile(`^[a-z0-9]{5}-dz642-[a-z0-9]{15}$`
 // Cancelled or Complete. See https://dev.arvados.org/issues/10979
 func (disp *Dispatcher) checkSqueueForOrphans() {
 	for _, uuid := range disp.sqCheck.All() {
-		if !containerUuidPattern.MatchString(uuid) {
+		if !containerUuidPattern.MatchString(uuid) || !strings.HasPrefix(uuid, disp.cluster.ClusterID) {
 			continue
 		}
 		err := disp.TrackContainer(uuid)

commit c088aa0b2f17315c93a6ea254c962b726df4b47e
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Dec 2 17:36:35 2020 -0500

    17009: Fix s3 ListObjects endpoint with vhost-style requests.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go
index 117389c4a..bcd1b55b2 100644
--- a/services/keep-web/s3.go
+++ b/services/keep-web/s3.go
@@ -206,11 +206,14 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 	fs.ForwardSlashNameSubstitution(h.Config.cluster.Collections.ForwardSlashNameSubstitution)
 
 	var objectNameGiven bool
+	var bucketName string
 	fspath := "/by_id"
 	if id := parseCollectionIDFromDNSName(r.Host); id != "" {
 		fspath += "/" + id
+		bucketName = id
 		objectNameGiven = strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 0
 	} else {
+		bucketName = strings.SplitN(strings.TrimPrefix(r.URL.Path, "/"), "/", 2)[0]
 		objectNameGiven = strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 1
 	}
 	fspath += r.URL.Path
@@ -225,7 +228,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 			fmt.Fprintln(w, `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>`)
 		} else {
 			// ListObjects
-			h.s3list(w, r, fs)
+			h.s3list(bucketName, w, r, fs)
 		}
 		return true
 	case r.Method == http.MethodGet || r.Method == http.MethodHead:
@@ -460,15 +463,13 @@ func walkFS(fs arvados.CustomFileSystem, path string, isRoot bool, fn func(path
 
 var errDone = errors.New("done")
 
-func (h *handler) s3list(w http.ResponseWriter, r *http.Request, fs arvados.CustomFileSystem) {
+func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request, fs arvados.CustomFileSystem) {
 	var params struct {
-		bucket    string
 		delimiter string
 		marker    string
 		maxKeys   int
 		prefix    string
 	}
-	params.bucket = strings.SplitN(r.URL.Path[1:], "/", 2)[0]
 	params.delimiter = r.FormValue("delimiter")
 	params.marker = r.FormValue("marker")
 	if mk, _ := strconv.ParseInt(r.FormValue("max-keys"), 10, 64); mk > 0 && mk < s3MaxKeys {
@@ -478,7 +479,7 @@ func (h *handler) s3list(w http.ResponseWriter, r *http.Request, fs arvados.Cust
 	}
 	params.prefix = r.FormValue("prefix")
 
-	bucketdir := "by_id/" + params.bucket
+	bucketdir := "by_id/" + bucket
 	// walkpath is the directory (relative to bucketdir) we need
 	// to walk: the innermost directory that is guaranteed to
 	// contain all paths that have the requested prefix. Examples:
@@ -513,7 +514,7 @@ func (h *handler) s3list(w http.ResponseWriter, r *http.Request, fs arvados.Cust
 	}
 	resp := listResp{
 		ListResp: s3.ListResp{
-			Name:      strings.SplitN(r.URL.Path[1:], "/", 2)[0],
+			Name:      bucket,
 			Prefix:    params.prefix,
 			Delimiter: params.delimiter,
 			Marker:    params.marker,

commit 5b5a5ad9bd96cccda87576c39e61cda3bcd57daa
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Thu Dec 3 16:24:37 2020 -0500

    17009: Test virtual host-style S3 requests.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/services/keep-web/s3_test.go b/services/keep-web/s3_test.go
index f8dc60086..07103d107 100644
--- a/services/keep-web/s3_test.go
+++ b/services/keep-web/s3_test.go
@@ -10,6 +10,8 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"os"
 	"os/exec"
 	"strings"
@@ -423,6 +425,70 @@ func (stage *s3stage) writeBigDirs(c *check.C, dirs int, filesPerDir int) {
 	c.Assert(fs.Sync(), check.IsNil)
 }
 
+func (s *IntegrationSuite) TestS3VirtualHostStyleRequests(c *check.C) {
+	stage := s.s3setup(c)
+	defer stage.teardown(c)
+	for _, trial := range []struct {
+		url            string
+		method         string
+		body           string
+		responseCode   int
+		responseRegexp []string
+	}{
+		{
+			url:            "https://" + stage.collbucket.Name + ".example.com/",
+			method:         "GET",
+			responseCode:   http.StatusOK,
+			responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
+		},
+		{
+			url:            "https://" + strings.Replace(stage.coll.PortableDataHash, "+", "-", -1) + ".example.com/",
+			method:         "GET",
+			responseCode:   http.StatusOK,
+			responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
+		},
+		{
+			url:            "https://" + stage.projbucket.Name + ".example.com/?prefix=" + stage.coll.Name + "/&delimiter=/",
+			method:         "GET",
+			responseCode:   http.StatusOK,
+			responseRegexp: []string{`(?ms).*sailboat\.txt.*`},
+		},
+		{
+			url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/sailboat.txt",
+			method:         "GET",
+			responseCode:   http.StatusOK,
+			responseRegexp: []string{`⛵\n`},
+		},
+		{
+			url:          "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/beep",
+			method:       "PUT",
+			body:         "boop",
+			responseCode: http.StatusOK,
+		},
+		{
+			url:            "https://" + stage.projbucket.Name + ".example.com/" + stage.coll.Name + "/beep",
+			method:         "GET",
+			responseCode:   http.StatusOK,
+			responseRegexp: []string{`boop`},
+		},
+	} {
+		url, err := url.Parse(trial.url)
+		c.Assert(err, check.IsNil)
+		req, err := http.NewRequest(trial.method, url.String(), bytes.NewReader([]byte(trial.body)))
+		c.Assert(err, check.IsNil)
+		req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
+		rr := httptest.NewRecorder()
+		s.testServer.Server.Handler.ServeHTTP(rr, req)
+		resp := rr.Result()
+		c.Check(resp.StatusCode, check.Equals, trial.responseCode)
+		body, err := ioutil.ReadAll(resp.Body)
+		c.Assert(err, check.IsNil)
+		for _, re := range trial.responseRegexp {
+			c.Check(string(body), check.Matches, re)
+		}
+	}
+}
+
 func (s *IntegrationSuite) TestS3GetBucketVersioning(c *check.C) {
 	stage := s.s3setup(c)
 	defer stage.teardown(c)

commit 2dab77693f534331324293aac49a36dc43119186
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Nov 25 12:14:34 2020 -0500

    17009: Fix bucket-level ops using virtual host-style requests.
    
    refs #17009
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go
index 57c9d7efb..117389c4a 100644
--- a/services/keep-web/s3.go
+++ b/services/keep-web/s3.go
@@ -209,7 +209,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 	fspath := "/by_id"
 	if id := parseCollectionIDFromDNSName(r.Host); id != "" {
 		fspath += "/" + id
-		objectNameGiven = true
+		objectNameGiven = strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 0
 	} else {
 		objectNameGiven = strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 1
 	}

commit e0fe85baa8500dc8eca281982eb92c1612046723
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Thu Nov 19 16:48:15 2020 -0500

    17009: Mention S3 considerations in keep-web install doc.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/doc/install/install-keep-web.html.textile.liquid b/doc/install/install-keep-web.html.textile.liquid
index 90ae6faa7..7f6770ad5 100644
--- a/doc/install/install-keep-web.html.textile.liquid
+++ b/doc/install/install-keep-web.html.textile.liquid
@@ -20,7 +20,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 h2(#introduction). Introduction
 
-The Keep-web server provides read/write HTTP (WebDAV) access to files stored in Keep.  This makes it easy to access files in Keep from a browser, or mount Keep as a network folder using WebDAV support in various operating systems. It serves public data to unauthenticated clients, and serves private data to clients that supply Arvados API tokens. It can be installed anywhere with access to Keep services, typically behind a web proxy that provides TLS support. See the "godoc page":http://godoc.org/github.com/curoverse/arvados/services/keep-web for more detail.
+The Keep-web server provides read/write access to files stored in Keep using WebDAV and S3 protocols.  This makes it easy to access files in Keep from a browser, or mount Keep as a network folder using WebDAV support in various operating systems. It serves public data to unauthenticated clients, and serves private data to clients that supply Arvados API tokens. It can be installed anywhere with access to Keep services, typically behind a web proxy that provides TLS support. See the "godoc page":http://godoc.org/github.com/curoverse/arvados/services/keep-web for more detail.
 
 h2(#dns). Configure DNS
 
@@ -61,6 +61,8 @@ Collections can be served from their own subdomain:
 </code></pre>
 </notextile>
 
+This option is preferred if you plan to access Keep using third-party S3 client software, because it accommodates S3 virtual host-style requests and path-style requests without any special client configuration.
+
 h4. Under the main domain
 
 Alternately, they can go under the main domain by including @--@:

commit 41b689b00a32e1f2a11e32125c7710bdc0887d99
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Wed Nov 18 17:35:29 2020 -0500

    17009: Support accessing S3 with virtual hosted-style URLs.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/doc/api/keep-s3.html.textile.liquid b/doc/api/keep-s3.html.textile.liquid
index 2cae81761..d5ad1dc60 100644
--- a/doc/api/keep-s3.html.textile.liquid
+++ b/doc/api/keep-s3.html.textile.liquid
@@ -21,7 +21,11 @@ To access Arvados S3 using an S3 client library, you must tell it to use the URL
 
 The "bucket name" is an Arvados collection uuid, portable data hash, or project uuid.
 
-The bucket name must be encoded as the first path segment of every request.  This is what the S3 documentation calls "Path-Style Requests".
+Path-style and virtual host-style requests are supported.
+* A path-style request uses the hostname indicated by @Services.WebDAVDownload.ExternalURL@, with the bucket name in the first path segment: @https://download.example.com/zzzzz-4zz18-asdfgasdfgasdfg/@.
+* A virtual host-style request uses the hostname pattern indicated by @Services.WebDAV.ExternalURL@, with a bucket name in place of the leading @*@: @https://zzzzz-4zz18-asdfgasdfgasdfg.collections.example.com/@.
+
+If you have wildcard DNS, TLS, and routing set up, an S3 client configured with endpoint @collections.example.com@ should work regardless of which request style it uses.
 
 h3. Supported Operations
 
diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go
index 49fb2456f..57c9d7efb 100644
--- a/services/keep-web/s3.go
+++ b/services/keep-web/s3.go
@@ -205,7 +205,15 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 	fs := client.SiteFileSystem(kc)
 	fs.ForwardSlashNameSubstitution(h.Config.cluster.Collections.ForwardSlashNameSubstitution)
 
-	objectNameGiven := strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 1
+	var objectNameGiven bool
+	fspath := "/by_id"
+	if id := parseCollectionIDFromDNSName(r.Host); id != "" {
+		fspath += "/" + id
+		objectNameGiven = true
+	} else {
+		objectNameGiven = strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 1
+	}
+	fspath += r.URL.Path
 
 	switch {
 	case r.Method == http.MethodGet && !objectNameGiven:
@@ -221,7 +229,6 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 		}
 		return true
 	case r.Method == http.MethodGet || r.Method == http.MethodHead:
-		fspath := "/by_id" + r.URL.Path
 		fi, err := fs.Stat(fspath)
 		if r.Method == "HEAD" && !objectNameGiven {
 			// HeadBucket
@@ -255,7 +262,6 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 			http.Error(w, "missing object name in PUT request", http.StatusBadRequest)
 			return true
 		}
-		fspath := "by_id" + r.URL.Path
 		var objectIsDir bool
 		if strings.HasSuffix(fspath, "/") {
 			if !h.Config.cluster.Collections.S3FolderObjects {
@@ -350,7 +356,6 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
 			http.Error(w, "missing object name in DELETE request", http.StatusBadRequest)
 			return true
 		}
-		fspath := "by_id" + r.URL.Path
 		if strings.HasSuffix(fspath, "/") {
 			fspath = strings.TrimSuffix(fspath, "/")
 			fi, err := fs.Stat(fspath)
diff --git a/services/keep-web/s3_test.go b/services/keep-web/s3_test.go
index 786e68afe..f8dc60086 100644
--- a/services/keep-web/s3_test.go
+++ b/services/keep-web/s3_test.go
@@ -700,3 +700,12 @@ func (s *IntegrationSuite) TestS3cmd(c *check.C) {
 	c.Check(err, check.IsNil)
 	c.Check(string(buf), check.Matches, `.* 3 +s3://`+arvadostest.FooCollection+`/foo\n`)
 }
+
+func (s *IntegrationSuite) TestS3BucketInHost(c *check.C) {
+	stage := s.s3setup(c)
+	defer stage.teardown(c)
+
+	hdr, body, _ := s.runCurl(c, "AWS "+arvadostest.ActiveTokenV2+":none", stage.coll.UUID+".collections.example.com", "/sailboat.txt")
+	c.Check(hdr, check.Matches, `(?s)HTTP/1.1 200 OK\r\n.*`)
+	c.Check(body, check.Equals, "⛵\n")
+}
diff --git a/services/keep-web/server_test.go b/services/keep-web/server_test.go
index acdc11b30..43817b51f 100644
--- a/services/keep-web/server_test.go
+++ b/services/keep-web/server_test.go
@@ -257,12 +257,16 @@ func (s *IntegrationSuite) Test200(c *check.C) {
 }
 
 // Return header block and body.
-func (s *IntegrationSuite) runCurl(c *check.C, token, host, uri string, args ...string) (hdr, bodyPart string, bodySize int64) {
+func (s *IntegrationSuite) runCurl(c *check.C, auth, host, uri string, args ...string) (hdr, bodyPart string, bodySize int64) {
 	curlArgs := []string{"--silent", "--show-error", "--include"}
 	testHost, testPort, _ := net.SplitHostPort(s.testServer.Addr)
 	curlArgs = append(curlArgs, "--resolve", host+":"+testPort+":"+testHost)
-	if token != "" {
-		curlArgs = append(curlArgs, "-H", "Authorization: OAuth2 "+token)
+	if strings.Contains(auth, " ") {
+		// caller supplied entire Authorization header value
+		curlArgs = append(curlArgs, "-H", "Authorization: "+auth)
+	} else if auth != "" {
+		// caller supplied Arvados token
+		curlArgs = append(curlArgs, "-H", "Authorization: Bearer "+auth)
 	}
 	curlArgs = append(curlArgs, args...)
 	curlArgs = append(curlArgs, "http://"+host+":"+testPort+uri)

commit 7da491928d7444147f4d864d5c101577e7f725e5
Author: Ward Vandewege <ward at curii.com>
Date:   Mon Jan 11 09:16:58 2021 -0500

    16106: address review comments.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/doc/admin/spot-instances.html.textile.liquid b/doc/admin/spot-instances.html.textile.liquid
index 7b4789295..07b721502 100644
--- a/doc/admin/spot-instances.html.textile.liquid
+++ b/doc/admin/spot-instances.html.textile.liquid
@@ -42,11 +42,11 @@ Clusters:
 
 When @UsePreemptibleInstances@ is enabled, child containers (workflow steps) will automatically be made preemptible.  Note that because preempting the workflow runner would cancel the entire workflow, the workflow runner runs in a reserved (non-preemptible) instance.
 
-No additional configuration is required, "arvados-dispatch-cloud":{{site.baseurl}}/install/crunch2-cloud/install-dispatch-cloud.html will now start preemptable instances where appropriate.
+No additional configuration is required, "arvados-dispatch-cloud":{{site.baseurl}}/install/crunch2-cloud/install-dispatch-cloud.html will now start preemptible instances where appropriate.
 
 h3. Cost Tracking
 
-Preemptable instances prices are declared at instance request time and defined by the maximum price that the user is willing to pay per hour. By default, this price is the same amount as the on-demand version of each instance type, and this setting is the one that @arvados-dispatch-cloud@ uses for now, as it doesn't include any pricing data to the spot instance request.
+Preemptible instances prices are declared at instance request time and defined by the maximum price that the user is willing to pay per hour. By default, this price is the same amount as the on-demand version of each instance type, and this setting is the one that @arvados-dispatch-cloud@ uses for now, as it doesn't include any pricing data to the spot instance request.
 
 For AWS, the real price that a spot instance has at any point in time is discovered at the end of each usage hour, depending on instance demand. For this reason, AWS provides a data feed subscription to get hourly logs, as described on "Amazon's User Guide":https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-data-feeds.html.
 
@@ -70,6 +70,6 @@ For general information, see "Use Spot VMs in Azure":https://docs.microsoft.com/
 
 When starting preemptible instances on Azure, Arvados configures the eviction policy to 'delete', with max price set to '-1'. This has the effect that preemptible VMs will not be evicted for pricing reasons. The price paid for the instance will be the current spot price for the VM type, up to a maximum of the price for a standard, non-spot VM of that type.
 
-Please note that Azure provides no SLA for preemptible instances. Even in this configuration, preemptable instances can still be evicted for capacity reasons. If that happens and a container is aborted, Arvados will try to restart it, subject to the usual retry rules.
+Please note that Azure provides no SLA for preemptible instances. Even in this configuration, preemptible instances can still be evicted for capacity reasons. If that happens and a container is aborted, Arvados will try to restart it, subject to the usual retry rules.
 
 Spot pricing is not available on 'B-series' VMs, those should not be defined in the configuration file with the _Preemptible_ flag set to true. Spot instances have a separate quota pool, make sure you have sufficient quota available.
diff --git a/lib/cloud/azure/azure.go b/lib/cloud/azure/azure.go
index 100d87c33..1ff0798ea 100644
--- a/lib/cloud/azure/azure.go
+++ b/lib/cloud/azure/azure.go
@@ -528,13 +528,12 @@ func (az *azureInstanceSet) Create(
 		},
 	}
 
-	var maxPrice float64
 	if instanceType.Preemptible {
 		// Setting maxPrice to -1 is the equivalent of paying spot price, up to the
 		// normal price. This means the node will not be pre-empted for price
 		// reasons. It may still be pre-empted for capacity reasons though. And
 		// Azure offers *no* SLA on spot instances.
-		maxPrice = -1
+		var maxPrice float64 = -1
 		vmParameters.VirtualMachineProperties.Priority = compute.Spot
 		vmParameters.VirtualMachineProperties.EvictionPolicy = compute.Delete
 		vmParameters.VirtualMachineProperties.BillingProfile = &compute.BillingProfile{MaxPrice: &maxPrice}

commit acf4fc32585cef9032b400abaa54582aed0ac76b
Author: Ward Vandewege <ward at curii.com>
Date:   Thu Jan 7 13:40:53 2021 -0500

    16106: update Azure preemptible node code after real world testing. Add
           documentation.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/doc/admin/spot-instances.html.textile.liquid b/doc/admin/spot-instances.html.textile.liquid
index 7f49d6961..7b4789295 100644
--- a/doc/admin/spot-instances.html.textile.liquid
+++ b/doc/admin/spot-instances.html.textile.liquid
@@ -12,11 +12,11 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 This page describes how to enable preemptible instances.  Preemptible instances typically offer lower cost computation with a tradeoff of lower service guarantees.  If a compute node is preempted, Arvados will restart the computation on a new instance.
 
-Currently Arvados supports preemptible instances using AWS spot instances.
+Currently Arvados supports preemptible instances using AWS and Azure spot instances.
 
 h2. Configuration
 
-To use preemptible instances, set @UsePreemptibleInstances: true@ and add entries to @InstanceTypes@ with @Preemptible: true@ to @config.yml at .  Typically you want to add both preemptible and non-preemptible entries for each cloud provider VM type.  The @Price@ for preemptible instances is the maximum bid price, the actual price paid is dynamic and may be lower.  For example:
+To use preemptible instances, set @UsePreemptibleInstances: true@ and add entries to @InstanceTypes@ with @Preemptible: true@ to @config.yml at .  Typically you want to add both preemptible and non-preemptible entries for each cloud provider VM type.  The @Price@ for preemptible instances is the maximum bid price, the actual price paid is dynamic and will likely be lower.  For example:
 
 <pre>
 Clusters:
@@ -42,11 +42,17 @@ Clusters:
 
 When @UsePreemptibleInstances@ is enabled, child containers (workflow steps) will automatically be made preemptible.  Note that because preempting the workflow runner would cancel the entire workflow, the workflow runner runs in a reserved (non-preemptible) instance.
 
-If you are using "arvados-dispatch-cloud":{{site.baseurl}}/install/crunch2-cloud/install-dispatch-cloud.html no additional configuration is required.
+No additional configuration is required, "arvados-dispatch-cloud":{{site.baseurl}}/install/crunch2-cloud/install-dispatch-cloud.html will now start preemptable instances where appropriate.
+
+h3. Cost Tracking
+
+Preemptable instances prices are declared at instance request time and defined by the maximum price that the user is willing to pay per hour. By default, this price is the same amount as the on-demand version of each instance type, and this setting is the one that @arvados-dispatch-cloud@ uses for now, as it doesn't include any pricing data to the spot instance request.
+
+For AWS, the real price that a spot instance has at any point in time is discovered at the end of each usage hour, depending on instance demand. For this reason, AWS provides a data feed subscription to get hourly logs, as described on "Amazon's User Guide":https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-data-feeds.html.
 
 h2. Preemptible instances on AWS
 
-For general information, see "using Amazon EC2 spot instances":https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html .
+For general information, see "using Amazon EC2 spot instances":https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html.
 
 h3. Permissions
 
@@ -58,9 +64,12 @@ BaseHTTPError: AuthFailure.ServiceLinkedRoleCreationNotPermitted: The provided c
 
 The account needs to have a service linked role created. This can be done by logging into the AWS account, go to _IAM Management_ → _Roles_ and create the @AWSServiceRoleForEC2Spot@ role by clicking on the @Create@ button, selecting @EC2@ service and @EC2 - Spot Instances@ use case.
 
-h3. Cost Tracking
+h2. Preemptible instances on Azure
+
+For general information, see "Use Spot VMs in Azure":https://docs.microsoft.com/en-us/azure/virtual-machines/spot-vms.
 
-Amazon's Spot instances prices are declared at instance request time and defined by the maximum price that the user is willing to pay per hour. By default, this price is the same amount as the on-demand version of each instance type, and this setting is the one that @arvados-dispatch-cloud@ uses for now, as it doesn't include any pricing data to the spot instance request.
+When starting preemptible instances on Azure, Arvados configures the eviction policy to 'delete', with max price set to '-1'. This has the effect that preemptible VMs will not be evicted for pricing reasons. The price paid for the instance will be the current spot price for the VM type, up to a maximum of the price for a standard, non-spot VM of that type.
 
-The real price that a spot instance has at any point in time is discovered at the end of each usage hour, depending on instance demand. For this reason, AWS provides a data feed subscription to get hourly logs, as described on "Amazon's User Guide":https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-data-feeds.html.
+Please note that Azure provides no SLA for preemptible instances. Even in this configuration, preemptable instances can still be evicted for capacity reasons. If that happens and a container is aborted, Arvados will try to restart it, subject to the usual retry rules.
 
+Spot pricing is not available on 'B-series' VMs, those should not be defined in the configuration file with the _Preemptible_ flag set to true. Spot instances have a separate quota pool, make sure you have sufficient quota available.
diff --git a/lib/cloud/azure/azure.go b/lib/cloud/azure/azure.go
index ad1f8c532..100d87c33 100644
--- a/lib/cloud/azure/azure.go
+++ b/lib/cloud/azure/azure.go
@@ -491,21 +491,6 @@ func (az *azureInstanceSet) Create(
 		}
 	}
 
-	priority := compute.Regular
-	evictionPolicy := compute.Deallocate
-	var billingProfile compute.BillingProfile
-	var maxPrice float64
-	if instanceType.Preemptible {
-		priority = compute.Spot
-		evictionPolicy = compute.Delete
-		// Setting maxPrice to -1 is the equivalent of paying spot price, up to the
-		// normal price. This means the node will not be pre-empted for price
-		// reasons. It may still be pre-empted for capacity reasons though. And
-		// Azure offers *no* SLA on spot instances.
-		maxPrice = -1
-		billingProfile = compute.BillingProfile{MaxPrice: &maxPrice}
-	}
-
 	vmParameters := compute.VirtualMachine{
 		Location: &az.azconfig.Location,
 		Tags:     tags,
@@ -514,9 +499,6 @@ func (az *azureInstanceSet) Create(
 				VMSize: compute.VirtualMachineSizeTypes(instanceType.ProviderType),
 			},
 			StorageProfile: storageProfile,
-			Priority:       priority,
-			EvictionPolicy: evictionPolicy,
-			BillingProfile: &billingProfile,
 			NetworkProfile: &compute.NetworkProfile{
 				NetworkInterfaces: &[]compute.NetworkInterfaceReference{
 					{
@@ -546,6 +528,18 @@ func (az *azureInstanceSet) Create(
 		},
 	}
 
+	var maxPrice float64
+	if instanceType.Preemptible {
+		// Setting maxPrice to -1 is the equivalent of paying spot price, up to the
+		// normal price. This means the node will not be pre-empted for price
+		// reasons. It may still be pre-empted for capacity reasons though. And
+		// Azure offers *no* SLA on spot instances.
+		maxPrice = -1
+		vmParameters.VirtualMachineProperties.Priority = compute.Spot
+		vmParameters.VirtualMachineProperties.EvictionPolicy = compute.Delete
+		vmParameters.VirtualMachineProperties.BillingProfile = &compute.BillingProfile{MaxPrice: &maxPrice}
+	}
+
 	vm, err := az.vmClient.createOrUpdate(az.ctx, az.azconfig.ResourceGroup, name, vmParameters)
 	if err != nil {
 		// Do some cleanup. Otherwise, an unbounded number of new unused nics and

commit cc22d5357dd0ca74036b52a50ea06597ee1c719f
Author: Ward Vandewege <ward at curii.com>
Date:   Fri Oct 2 20:49:22 2020 -0400

    16106: add Azure spot instance support.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/lib/cloud/azure/azure.go b/lib/cloud/azure/azure.go
index 7f949d9bd..ad1f8c532 100644
--- a/lib/cloud/azure/azure.go
+++ b/lib/cloud/azure/azure.go
@@ -491,6 +491,21 @@ func (az *azureInstanceSet) Create(
 		}
 	}
 
+	priority := compute.Regular
+	evictionPolicy := compute.Deallocate
+	var billingProfile compute.BillingProfile
+	var maxPrice float64
+	if instanceType.Preemptible {
+		priority = compute.Spot
+		evictionPolicy = compute.Delete
+		// Setting maxPrice to -1 is the equivalent of paying spot price, up to the
+		// normal price. This means the node will not be pre-empted for price
+		// reasons. It may still be pre-empted for capacity reasons though. And
+		// Azure offers *no* SLA on spot instances.
+		maxPrice = -1
+		billingProfile = compute.BillingProfile{MaxPrice: &maxPrice}
+	}
+
 	vmParameters := compute.VirtualMachine{
 		Location: &az.azconfig.Location,
 		Tags:     tags,
@@ -499,6 +514,9 @@ func (az *azureInstanceSet) Create(
 				VMSize: compute.VirtualMachineSizeTypes(instanceType.ProviderType),
 			},
 			StorageProfile: storageProfile,
+			Priority:       priority,
+			EvictionPolicy: evictionPolicy,
+			BillingProfile: &billingProfile,
 			NetworkProfile: &compute.NetworkProfile{
 				NetworkInterfaces: &[]compute.NetworkInterfaceReference{
 					{
diff --git a/lib/cloud/azure/azure_test.go b/lib/cloud/azure/azure_test.go
index 96d6dca69..b6aa9a16b 100644
--- a/lib/cloud/azure/azure_test.go
+++ b/lib/cloud/azure/azure_test.go
@@ -136,6 +136,15 @@ func GetInstanceSet() (cloud.InstanceSet, cloud.ImageID, arvados.Cluster, error)
 				Price:        .02,
 				Preemptible:  false,
 			},
+			"tinyp": {
+				Name:         "tiny",
+				ProviderType: "Standard_D1_v2",
+				VCPUs:        1,
+				RAM:          4000000000,
+				Scratch:      10000000000,
+				Price:        .002,
+				Preemptible:  true,
+			},
 		})}
 	if *live != "" {
 		var exampleCfg testConfig
@@ -185,6 +194,17 @@ func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
 	c.Check(tags["TestTagName"], check.Equals, "test tag value")
 	c.Logf("inst.String()=%v Address()=%v Tags()=%v", inst.String(), inst.Address(), tags)
 
+	instPreemptable, err := ap.Create(cluster.InstanceTypes["tinyp"],
+		img, map[string]string{
+			"TestTagName": "test tag value",
+		}, "umask 0600; echo -n test-file-data >/var/run/test-file", pk)
+
+	c.Assert(err, check.IsNil)
+
+	tags = instPreemptable.Tags()
+	c.Check(tags["TestTagName"], check.Equals, "test tag value")
+	c.Logf("instPreemptable.String()=%v Address()=%v Tags()=%v", instPreemptable.String(), instPreemptable.Address(), tags)
+
 }
 
 func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {

commit cffdc810f4cf76eba807acbd5dd0539b1c77480a
Author: Ward Vandewege <ward at curii.com>
Date:   Wed Jan 20 16:16:11 2021 -0500

    17215: add IAM role support to arvados-dispatch-cloud on EC2.
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/doc/install/crunch2-cloud/install-dispatch-cloud.html.textile.liquid b/doc/install/crunch2-cloud/install-dispatch-cloud.html.textile.liquid
index a2186a42f..51d4f8fbc 100644
--- a/doc/install/crunch2-cloud/install-dispatch-cloud.html.textile.liquid
+++ b/doc/install/crunch2-cloud/install-dispatch-cloud.html.textile.liquid
@@ -82,8 +82,12 @@ The <span class="userinput">ImageID</span> value is the compute node image that
         ImageID: <span class="userinput">ami-01234567890abcdef</span>
         Driver: ec2
         DriverParameters:
+          # If you are not using an IAM role for authentication, specify access
+          # credentials here. Otherwise, omit or set AccessKeyID and
+          # SecretAccessKey to an empty value.
           AccessKeyID: XXXXXXXXXXXXXXXXXXXX
           SecretAccessKey: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
+
           SecurityGroupIDs:
           - sg-0123abcd
           SubnetID: subnet-0123abcd
diff --git a/lib/cloud/ec2/ec2.go b/lib/cloud/ec2/ec2.go
index 29062c491..66c8d672c 100644
--- a/lib/cloud/ec2/ec2.go
+++ b/lib/cloud/ec2/ec2.go
@@ -19,6 +19,8 @@ import (
 	"git.arvados.org/arvados.git/sdk/go/arvados"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
+	"github.com/aws/aws-sdk-go/aws/ec2metadata"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/sirupsen/logrus"
@@ -65,12 +67,19 @@ func newEC2InstanceSet(config json.RawMessage, instanceSetID cloud.InstanceSetID
 	if err != nil {
 		return nil, err
 	}
-	awsConfig := aws.NewConfig().
-		WithCredentials(credentials.NewStaticCredentials(
-			instanceSet.ec2config.AccessKeyID,
-			instanceSet.ec2config.SecretAccessKey,
-			"")).
-		WithRegion(instanceSet.ec2config.Region)
+
+	sess, err := session.NewSession()
+	if err != nil {
+		return nil, err
+	}
+	// First try any static credentials, fall back to an IAM instance profile/role
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.StaticProvider{Value: credentials.Value{AccessKeyID: instanceSet.ec2config.AccessKeyID, SecretAccessKey: instanceSet.ec2config.SecretAccessKey}},
+			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess)},
+		})
+
+	awsConfig := aws.NewConfig().WithCredentials(creds).WithRegion(instanceSet.ec2config.Region)
 	instanceSet.client = ec2.New(session.Must(session.NewSession(awsConfig)))
 	instanceSet.keys = make(map[string]string)
 	if instanceSet.ec2config.EBSVolumeType == "" {
diff --git a/lib/config/config.default.yml b/lib/config/config.default.yml
index e1afcd69f..f7d874237 100644
--- a/lib/config/config.default.yml
+++ b/lib/config/config.default.yml
@@ -1062,7 +1062,7 @@ Clusters:
         # Cloud-specific driver parameters.
         DriverParameters:
 
-          # (ec2) Credentials.
+          # (ec2) Credentials. Omit or leave blank if using IAM role.
           AccessKeyID: ""
           SecretAccessKey: ""
 
diff --git a/lib/config/generated_config.go b/lib/config/generated_config.go
index a4b997c26..6952cabe0 100644
--- a/lib/config/generated_config.go
+++ b/lib/config/generated_config.go
@@ -1068,7 +1068,7 @@ Clusters:
         # Cloud-specific driver parameters.
         DriverParameters:
 
-          # (ec2) Credentials.
+          # (ec2) Credentials. Omit or leave blank if using IAM role.
           AccessKeyID: ""
           SecretAccessKey: ""
 

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list