[ARVADOS] created: 2.1.0-93-g9e3127538

Git user git at public.arvados.org
Tue Nov 10 14:07:07 UTC 2020


        at  9e312753855286a7e21c83218bb48589f4d1535d (commit)


commit 9e312753855286a7e21c83218bb48589f4d1535d
Author: Nico Cesar <nico at nicocesar.com>
Date:   Tue Nov 10 09:00:52 2020 -0500

    container requests in the new codepath for controller
    
    Controller's "new" codepath is a step towards sunsetting railsapi. This
    commit is the initial work to get Container Requests in that path
    
    The reason for deleting some tests is intentional. This is part of the migration
    from
       apps/workbench/test/controllers/container_requests_controller_test.rb
    to
       lib/controller/integration_test.go
    
    This single-commit is the result of collaboration with Lucas Di Pentima
    and Tom Clegg in the branch 17014-controller-cr
    
    Arvados-DCO-1.1-Signed-off-by: Nico Cesar <nico at curii.com>

diff --git a/apps/workbench/test/controllers/container_requests_controller_test.rb b/apps/workbench/test/controllers/container_requests_controller_test.rb
index 73d357f3a..c8709df3c 100644
--- a/apps/workbench/test/controllers/container_requests_controller_test.rb
+++ b/apps/workbench/test/controllers/container_requests_controller_test.rb
@@ -138,7 +138,6 @@ class ContainerRequestsControllerTest < ActionController::TestCase
     assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/foobar\?" # locator on command
     assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/foo" # mount input1
     assert_includes @response.body, "href=\"\/collections/fa7aeb5140e2848d39b416daeef4ffc5+45/bar" # mount input2
-    assert_includes @response.body, "href=\"\/collections/f9ddda46bb293b6847da984e3aa735db+290" # mount workflow
     assert_includes @response.body, "href=\"#Log\""
     assert_includes @response.body, "href=\"#Provenance\""
   end
diff --git a/lib/boot/supervisor.go b/lib/boot/supervisor.go
index 1e8e83ff3..0e4de9d5c 100644
--- a/lib/boot/supervisor.go
+++ b/lib/boot/supervisor.go
@@ -59,6 +59,8 @@ type Supervisor struct {
 	environ    []string // for child processes
 }
 
+func (super *Supervisor) Cluster() *arvados.Cluster { return super.cluster }
+
 func (super *Supervisor) Start(ctx context.Context, cfg *arvados.Config, cfgPath string) {
 	super.ctx, super.cancel = context.WithCancel(ctx)
 	super.done = make(chan struct{})
diff --git a/lib/controller/cmd.go b/lib/controller/cmd.go
index 0c46e857b..c6b53eb8f 100644
--- a/lib/controller/cmd.go
+++ b/lib/controller/cmd.go
@@ -13,6 +13,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 )
 
+// Command intanciates a cmd.Handler with the service.Command used in package cmd
 var Command cmd.Handler = service.Command(arvados.ServiceNameController, newHandler)
 
 func newHandler(_ context.Context, cluster *arvados.Cluster, _ string, _ *prometheus.Registry) service.Handler {
diff --git a/lib/controller/fed_containers.go b/lib/controller/fed_containers.go
index 51f243e69..028f4f597 100644
--- a/lib/controller/fed_containers.go
+++ b/lib/controller/fed_containers.go
@@ -28,7 +28,6 @@ func remoteContainerRequestCreate(
 	if effectiveMethod != "POST" || uuid != "" || remainder != "" {
 		return false
 	}
-
 	// First make sure supplied token is valid.
 	creds := auth.NewCredentials()
 	creds.LoadTokensFromHTTPRequest(req)
diff --git a/lib/controller/federation.go b/lib/controller/federation.go
index cab5e4c4c..869fec58e 100644
--- a/lib/controller/federation.go
+++ b/lib/controller/federation.go
@@ -164,6 +164,12 @@ func (h *Handler) validateAPItoken(req *http.Request, token string) (*CurrentUse
 		uuid = sp[1]
 		token = sp[2]
 	}
+
+	if len(token) < 41 {
+		ctxlog.FromContext(req.Context()).Debugf("validateAPItoken(%s): The lenght of the token is not 41", token)
+		return nil, false, nil
+	}
+
 	user.Authorization.APIToken = token
 	var scopes string
 	err = db.QueryRowContext(req.Context(), `SELECT api_client_authorizations.uuid, api_client_authorizations.scopes, users.uuid FROM api_client_authorizations JOIN users on api_client_authorizations.user_id=users.id WHERE api_token=$1 AND (expires_at IS NULL OR expires_at > current_timestamp AT TIME ZONE 'UTC') LIMIT 1`, token).Scan(&user.Authorization.UUID, &scopes, &user.UUID)
diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go
index 986faa7b0..446e3893d 100644
--- a/lib/controller/federation/conn.go
+++ b/lib/controller/federation/conn.go
@@ -336,6 +336,66 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
 	return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
 }
 
+func (conn *Conn) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
+	return conn.generated_ContainerRequestList(ctx, options)
+}
+
+func (conn *Conn) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
+	be := conn.chooseBackend(options.ClusterID)
+	if be == conn.local {
+		return be.ContainerRequestCreate(ctx, options)
+	}
+	if _, ok := options.Attrs["runtime_token"]; !ok {
+		// If runtime_token is not set, create a new token
+		aca, err := conn.local.APIClientAuthorizationCurrent(ctx, arvados.GetOptions{})
+		if err != nil {
+			// This should probably be StatusUnauthorized
+			// (need to update test in
+			// lib/controller/federation_test.go):
+			return arvados.ContainerRequest{}, httpErrorf(http.StatusForbidden, "%w", err)
+		}
+		user, err := conn.local.UserGetCurrent(ctx, arvados.GetOptions{})
+		if err != nil {
+			return arvados.ContainerRequest{}, err
+		}
+		if len(aca.Scopes) != 0 || aca.Scopes[0] != "all" {
+			return arvados.ContainerRequest{}, httpErrorf(http.StatusForbidden, "token scope is not [all]")
+		}
+		if strings.HasPrefix(aca.UUID, conn.cluster.ClusterID) {
+			// Local user, submitting to a remote cluster.
+			// Create a new (FIXME: needs to be
+			// time-limited!) token.
+			local, ok := conn.local.(*localdb.Conn)
+			if !ok {
+				return arvados.ContainerRequest{}, httpErrorf(http.StatusInternalServerError, "bug: local backend is a %T, not a *localdb.Conn", conn.local)
+			}
+			aca, err = local.CreateAPIClientAuthorization(ctx, conn.cluster.SystemRootToken, rpc.UserSessionAuthInfo{UserUUID: user.UUID})
+			if err != nil {
+				return arvados.ContainerRequest{}, err
+			}
+			options.Attrs["runtime_token"] = aca.TokenV2()
+		} else {
+			// Remote user. Container request will use the
+			// current token, minus the trailing portion
+			// (optional container uuid).
+			options.Attrs["runtime_token"] = aca.TokenV2()
+		}
+	}
+	return be.ContainerRequestCreate(ctx, options)
+}
+
+func (conn *Conn) ContainerRequestUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.ContainerRequest, error) {
+	return conn.chooseBackend(options.UUID).ContainerRequestUpdate(ctx, options)
+}
+
+func (conn *Conn) ContainerRequestGet(ctx context.Context, options arvados.GetOptions) (arvados.ContainerRequest, error) {
+	return conn.chooseBackend(options.UUID).ContainerRequestGet(ctx, options)
+}
+
+func (conn *Conn) ContainerRequestDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.ContainerRequest, error) {
+	return conn.chooseBackend(options.UUID).ContainerRequestDelete(ctx, options)
+}
+
 func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
 	return conn.generated_SpecimenList(ctx, options)
 }
diff --git a/lib/controller/federation/generate.go b/lib/controller/federation/generate.go
index ab5d9966a..9ce7fdcb2 100644
--- a/lib/controller/federation/generate.go
+++ b/lib/controller/federation/generate.go
@@ -52,7 +52,7 @@ func main() {
 		defer out.Close()
 		out.Write(regexp.MustCompile(`(?ms)^.*package .*?import.*?\n\)\n`).Find(buf))
 		io.WriteString(out, "//\n// -- this file is auto-generated -- do not edit -- edit list.go and run \"go generate\" instead --\n//\n\n")
-		for _, t := range []string{"Container", "Specimen", "User"} {
+		for _, t := range []string{"Container", "ContainerRequest", "Specimen", "User"} {
 			_, err := out.Write(bytes.ReplaceAll(orig, []byte("Collection"), []byte(t)))
 			if err != nil {
 				panic(err)
diff --git a/lib/controller/federation/generated.go b/lib/controller/federation/generated.go
index 8745f3b97..ab9db93a4 100755
--- a/lib/controller/federation/generated.go
+++ b/lib/controller/federation/generated.go
@@ -58,6 +58,47 @@ func (conn *Conn) generated_ContainerList(ctx context.Context, options arvados.L
 	return merged, err
 }
 
+func (conn *Conn) generated_ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
+	var mtx sync.Mutex
+	var merged arvados.ContainerRequestList
+	var needSort atomic.Value
+	needSort.Store(false)
+	err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
+		options.ForwardedFor = conn.cluster.ClusterID + "-" + options.ForwardedFor
+		cl, err := backend.ContainerRequestList(ctx, options)
+		if err != nil {
+			return nil, err
+		}
+		mtx.Lock()
+		defer mtx.Unlock()
+		if len(merged.Items) == 0 {
+			merged = cl
+		} else if len(cl.Items) > 0 {
+			merged.Items = append(merged.Items, cl.Items...)
+			needSort.Store(true)
+		}
+		uuids := make([]string, 0, len(cl.Items))
+		for _, item := range cl.Items {
+			uuids = append(uuids, item.UUID)
+		}
+		return uuids, nil
+	})
+	if needSort.Load().(bool) {
+		// Apply the default/implied order, "modified_at desc"
+		sort.Slice(merged.Items, func(i, j int) bool {
+			mi, mj := merged.Items[i].ModifiedAt, merged.Items[j].ModifiedAt
+			return mj.Before(mi)
+		})
+	}
+	if merged.Items == nil {
+		// Return empty results as [], not null
+		// (https://github.com/golang/go/issues/27589 might be
+		// a better solution in the future)
+		merged.Items = []arvados.ContainerRequest{}
+	}
+	return merged, err
+}
+
 func (conn *Conn) generated_SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
 	var mtx sync.Mutex
 	var merged arvados.SpecimenList
diff --git a/lib/controller/federation_test.go b/lib/controller/federation_test.go
index 6a9ad8c15..891e49180 100644
--- a/lib/controller/federation_test.go
+++ b/lib/controller/federation_test.go
@@ -587,139 +587,6 @@ func (s *FederationSuite) TestUpdateRemoteContainerRequest(c *check.C) {
 	setPri(1) // Reset fixture so side effect doesn't break other tests.
 }
 
-func (s *FederationSuite) TestCreateContainerRequestBadToken(c *check.C) {
-	defer s.localServiceReturns404(c).Close()
-	// pass cluster_id via query parameter, this allows arvados-controller
-	// to avoid parsing the body
-	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zzzzz",
-		strings.NewReader(`{"container_request":{}}`))
-	req.Header.Set("Authorization", "Bearer abcdefg")
-	req.Header.Set("Content-type", "application/json")
-	resp := s.testRequest(req).Result()
-	c.Check(resp.StatusCode, check.Equals, http.StatusForbidden)
-	var e map[string][]string
-	c.Check(json.NewDecoder(resp.Body).Decode(&e), check.IsNil)
-	c.Check(e["errors"], check.DeepEquals, []string{"invalid API token"})
-}
-
-func (s *FederationSuite) TestCreateRemoteContainerRequest(c *check.C) {
-	defer s.localServiceReturns404(c).Close()
-	// pass cluster_id via query parameter, this allows arvados-controller
-	// to avoid parsing the body
-	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zzzzz",
-		strings.NewReader(`{
-  "container_request": {
-    "name": "hello world",
-    "state": "Uncommitted",
-    "output_path": "/",
-    "container_image": "123",
-    "command": ["abc"]
-  }
-}
-`))
-	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-	req.Header.Set("Content-type", "application/json")
-	resp := s.testRequest(req).Result()
-	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
-	var cr arvados.ContainerRequest
-	c.Check(json.NewDecoder(resp.Body).Decode(&cr), check.IsNil)
-	c.Check(cr.Name, check.Equals, "hello world")
-	c.Check(strings.HasPrefix(cr.UUID, "zzzzz-"), check.Equals, true)
-}
-
-func (s *FederationSuite) TestCreateRemoteContainerRequestCheckRuntimeToken(c *check.C) {
-	// Send request to zmock and check that outgoing request has
-	// runtime_token set with a new random v2 token.
-
-	defer s.localServiceReturns404(c).Close()
-	// pass cluster_id via query parameter, this allows arvados-controller
-	// to avoid parsing the body
-	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zmock",
-		strings.NewReader(`{
-  "container_request": {
-    "name": "hello world",
-    "state": "Uncommitted",
-    "output_path": "/",
-    "container_image": "123",
-    "command": ["abc"]
-  }
-}
-`))
-	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
-	req.Header.Set("Content-type", "application/json")
-
-	arvadostest.SetServiceURL(&s.testHandler.Cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
-	s.testHandler.Cluster.ClusterID = "zzzzz"
-
-	resp := s.testRequest(req).Result()
-	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
-	var cr struct {
-		arvados.ContainerRequest `json:"container_request"`
-	}
-	c.Check(json.NewDecoder(s.remoteMockRequests[0].Body).Decode(&cr), check.IsNil)
-	c.Check(strings.HasPrefix(cr.ContainerRequest.RuntimeToken, "v2/zzzzz-gj3su-"), check.Equals, true)
-	c.Check(cr.ContainerRequest.RuntimeToken, check.Not(check.Equals), arvadostest.ActiveTokenV2)
-}
-
-func (s *FederationSuite) TestCreateRemoteContainerRequestCheckSetRuntimeToken(c *check.C) {
-	// Send request to zmock and check that outgoing request has
-	// runtime_token set with the explicitly provided token.
-
-	defer s.localServiceReturns404(c).Close()
-	// pass cluster_id via query parameter, this allows arvados-controller
-	// to avoid parsing the body
-	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zmock",
-		strings.NewReader(`{
-  "container_request": {
-    "name": "hello world",
-    "state": "Uncommitted",
-    "output_path": "/",
-    "container_image": "123",
-    "command": ["abc"],
-    "runtime_token": "xyz"
-  }
-}
-`))
-	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveToken)
-	req.Header.Set("Content-type", "application/json")
-	resp := s.testRequest(req).Result()
-	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
-	var cr struct {
-		arvados.ContainerRequest `json:"container_request"`
-	}
-	c.Check(json.NewDecoder(s.remoteMockRequests[0].Body).Decode(&cr), check.IsNil)
-	c.Check(cr.ContainerRequest.RuntimeToken, check.Equals, "xyz")
-}
-
-func (s *FederationSuite) TestCreateRemoteContainerRequestRuntimeTokenFromAuth(c *check.C) {
-	// Send request to zmock and check that outgoing request has
-	// runtime_token set using the Auth token because the user is remote.
-
-	defer s.localServiceReturns404(c).Close()
-	// pass cluster_id via query parameter, this allows arvados-controller
-	// to avoid parsing the body
-	req := httptest.NewRequest("POST", "/arvados/v1/container_requests?cluster_id=zmock",
-		strings.NewReader(`{
-  "container_request": {
-    "name": "hello world",
-    "state": "Uncommitted",
-    "output_path": "/",
-    "container_image": "123",
-    "command": ["abc"]
-  }
-}
-`))
-	req.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2+"/zzzzz-dz642-parentcontainer")
-	req.Header.Set("Content-type", "application/json")
-	resp := s.testRequest(req).Result()
-	c.Check(resp.StatusCode, check.Equals, http.StatusOK)
-	var cr struct {
-		arvados.ContainerRequest `json:"container_request"`
-	}
-	c.Check(json.NewDecoder(s.remoteMockRequests[0].Body).Decode(&cr), check.IsNil)
-	c.Check(cr.ContainerRequest.RuntimeToken, check.Equals, arvadostest.ActiveTokenV2)
-}
-
 func (s *FederationSuite) TestCreateRemoteContainerRequestError(c *check.C) {
 	defer s.localServiceReturns404(c).Close()
 	// pass cluster_id via query parameter, this allows arvados-controller
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index 25bba558d..2b2d72f4f 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -99,6 +99,8 @@ func (h *Handler) setup() {
 		mux.Handle("/arvados/v1/collections/", rtr)
 		mux.Handle("/arvados/v1/users", rtr)
 		mux.Handle("/arvados/v1/users/", rtr)
+		mux.Handle("/arvados/v1/container_requests", rtr)
+		mux.Handle("/arvados/v1/container_requests/", rtr)
 		mux.Handle("/login", rtr)
 		mux.Handle("/logout", rtr)
 	}
diff --git a/lib/controller/handler_test.go b/lib/controller/handler_test.go
index 7d8266a85..d12e4fa33 100644
--- a/lib/controller/handler_test.go
+++ b/lib/controller/handler_test.go
@@ -294,6 +294,8 @@ func (s *HandlerSuite) CheckObjectType(c *check.C, url string, token string, ski
 	}
 	resp2, err := client.Get(s.cluster.Services.RailsAPI.ExternalURL.String() + url + "/?api_token=" + token)
 	c.Check(err, check.Equals, nil)
+	c.Assert(resp2.StatusCode, check.Equals, http.StatusOK,
+		check.Commentf("Wasn't able to get data from the RailsAPI at %q", url))
 	defer resp2.Body.Close()
 	db, err := ioutil.ReadAll(resp2.Body)
 	c.Check(err, check.Equals, nil)
diff --git a/lib/controller/integration_test.go b/lib/controller/integration_test.go
index 3da01ca68..6115ba580 100644
--- a/lib/controller/integration_test.go
+++ b/lib/controller/integration_test.go
@@ -7,6 +7,7 @@ package controller
 import (
 	"bytes"
 	"context"
+	"database/sql"
 	"encoding/json"
 	"io"
 	"io/ioutil"
@@ -383,6 +384,7 @@ func (s *IntegrationSuite) TestCreateContainerRequestWithFedToken(c *check.C) {
 	c.Assert(err, check.IsNil)
 	req.Header.Set("Content-Type", "application/json")
 	err = ac2.DoAndDecode(&cr, req)
+	c.Assert(err, check.IsNil)
 	c.Logf("err == %#v", err)
 
 	c.Log("...get user with good token")
@@ -409,10 +411,131 @@ func (s *IntegrationSuite) TestCreateContainerRequestWithFedToken(c *check.C) {
 	req.Header.Set("Content-Type", "application/json")
 	req.Header.Set("Authorization", "OAuth2 "+ac2.AuthToken)
 	resp, err = arvados.InsecureHTTPClient.Do(req)
-	if c.Check(err, check.IsNil) {
-		err = json.NewDecoder(resp.Body).Decode(&cr)
-		c.Check(err, check.IsNil)
-		c.Check(cr.UUID, check.Matches, "z2222-.*")
+	c.Assert(err, check.IsNil)
+	err = json.NewDecoder(resp.Body).Decode(&cr)
+	c.Check(err, check.IsNil)
+	c.Check(cr.UUID, check.Matches, "z2222-.*")
+}
+
+func (s *IntegrationSuite) TestCreateContainerRequestWithBadToken(c *check.C) {
+	var (
+		body bytes.Buffer
+		resp *http.Response
+	)
+
+	conn1 := s.conn("z1111")
+	rootctx1, _, _ := s.rootClients("z1111")
+	_, ac1, _, au := s.userClients(rootctx1, c, conn1, "z1111", true)
+
+	tests := []struct {
+		name         string
+		token        string
+		expectedCode int
+	}{
+		{"Good token", ac1.AuthToken, http.StatusOK},
+		{"Bogus token", "abcdef", http.StatusUnauthorized},
+		{"v1-looking token", "badtoken00badtoken00badtoken00badtoken00b", http.StatusUnauthorized},
+		{"v2-looking token", "v2/" + au.UUID + "/badtoken00badtoken00badtoken00badtoken00b", http.StatusUnauthorized},
+	}
+
+	json.NewEncoder(&body).Encode(map[string]interface{}{
+		"container_request": map[string]interface{}{
+			"command":         []string{"echo"},
+			"container_image": "d41d8cd98f00b204e9800998ecf8427e+0",
+			"cwd":             "/",
+			"output_path":     "/",
+		},
+	})
+
+	for _, tt := range tests {
+		c.Log(c.TestName() + " " + tt.name)
+		ac1.AuthToken = tt.token
+		req, err := http.NewRequest("POST", "https://"+ac1.APIHost+"/arvados/v1/container_requests", bytes.NewReader(body.Bytes()))
+		c.Assert(err, check.IsNil)
+		req.Header.Set("Content-Type", "application/json")
+		resp, err = ac1.Do(req)
+		c.Assert(err, check.IsNil)
+		c.Assert(resp.StatusCode, check.Equals, tt.expectedCode)
+	}
+}
+
+// We test the direct access to the database
+// normally an integration test would not have a database access, but  in this case we need
+// to test tokens that are secret, so there is no API response that will give them back
+func (s *IntegrationSuite) TestDatabaseConnection(c *check.C) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	db, err := sql.Open("postgres", s.testClusters["z1111"].super.Cluster().PostgreSQL.Connection.String())
+	c.Assert(err, check.IsNil)
+	defer db.Close()
+	conn, err := db.Conn(ctx)
+	c.Assert(err, check.IsNil)
+	defer conn.Close()
+	rows, err := conn.ExecContext(ctx, `SELECT 1`)
+	c.Assert(err, check.IsNil)
+	n, err := rows.RowsAffected()
+	c.Assert(err, check.IsNil)
+	c.Assert(n, check.Equals, int64(1))
+}
+
+func (s *IntegrationSuite) TestRuntimeTokenInCR(c *check.C) {
+	conn1 := s.conn("z1111")
+	rootctx1, _, _ := s.rootClients("z1111")
+	_, ac1, _, au := s.userClients(rootctx1, c, conn1, "z1111", true)
+
+	tests := []struct {
+		name                 string
+		token                string
+		expectAToGetAValidCR bool
+		expectedToken        *string
+	}{
+		{"Good token z1111 user", ac1.AuthToken, true, &ac1.AuthToken},
+		{"Bogus token", "abcdef", false, nil},
+		{"v1-looking token", "badtoken00badtoken00badtoken00badtoken00b", false, nil},
+		{"v2-looking token", "v2/" + au.UUID + "/badtoken00badtoken00badtoken00badtoken00b", false, nil},
+	}
+
+	for _, tt := range tests {
+		c.Log(c.TestName() + " " + tt.name)
+
+		rq := map[string]interface{}{
+			"command":         []string{"echo"},
+			"container_image": "d41d8cd98f00b204e9800998ecf8427e+0",
+			"cwd":             "/",
+			"output_path":     "/",
+			"runtime_token":   tt.token,
+		}
+		cr, err := conn1.ContainerRequestCreate(rootctx1, arvados.CreateOptions{Attrs: rq})
+		if tt.expectAToGetAValidCR {
+			c.Assert(err, check.IsNil)
+			c.Assert(cr, check.NotNil)
+			c.Assert(cr.UUID, check.Not(check.Equals), "")
+		}
+
+		if tt.expectedToken == nil {
+			break
+		}
+
+		ctx2, cancel2 := context.WithCancel(context.Background())
+		defer cancel2()
+
+		db, err := sql.Open("postgres", s.testClusters["z1111"].super.Cluster().PostgreSQL.Connection.String())
+		c.Assert(err, check.IsNil)
+		defer db.Close()
+
+		conn, err := db.Conn(ctx2)
+		c.Assert(err, check.IsNil)
+		defer conn.Close()
+
+		c.Logf("cr.UUID: %s", cr.UUID)
+		row := conn.QueryRowContext(ctx2, `SELECT runtime_token from container_requests where uuid=$1`, cr.UUID)
+		c.Assert(row, check.NotNil)
+		// runtimeToken is *string and not a string because the database has a NULL column for this
+		var runtimeToken *string
+		err = row.Scan(&runtimeToken)
+		c.Assert(err, check.IsNil)
+		c.Assert(runtimeToken, check.NotNil)
+		c.Assert(*runtimeToken, check.DeepEquals, *tt.expectedToken)
 	}
 }
 
diff --git a/lib/controller/localdb/conn.go b/lib/controller/localdb/conn.go
index 4f0035edf..73bed2184 100644
--- a/lib/controller/localdb/conn.go
+++ b/lib/controller/localdb/conn.go
@@ -24,21 +24,24 @@ func NewConn(cluster *arvados.Cluster) *Conn {
 	railsProxy := railsproxy.NewConn(cluster)
 	var conn Conn
 	conn = Conn{
-		cluster:         cluster,
-		railsProxy:      railsProxy,
-		loginController: chooseLoginController(cluster, railsProxy),
+		cluster:    cluster,
+		railsProxy: railsProxy,
 	}
+	conn.loginController = chooseLoginController(cluster, &conn)
 	return &conn
 }
 
+// Logout handles the logout of conn giving to the appropiate loginController
 func (conn *Conn) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
 	return conn.loginController.Logout(ctx, opts)
 }
 
+// Login handles the logout of conn giving to the appropiate loginController
 func (conn *Conn) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
 	return conn.loginController.Login(ctx, opts)
 }
 
+// UserAuthenticate handles the User Authentication of conn giving to the appropiate loginController
 func (conn *Conn) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
 	return conn.loginController.UserAuthenticate(ctx, opts)
 }
diff --git a/lib/controller/localdb/login.go b/lib/controller/localdb/login.go
index f4632751e..61bad465f 100644
--- a/lib/controller/localdb/login.go
+++ b/lib/controller/localdb/login.go
@@ -27,7 +27,7 @@ type loginController interface {
 	UserAuthenticate(ctx context.Context, options arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error)
 }
 
-func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) loginController {
+func chooseLoginController(cluster *arvados.Cluster, parent *Conn) loginController {
 	wantGoogle := cluster.Login.Google.Enable
 	wantOpenIDConnect := cluster.Login.OpenIDConnect.Enable
 	wantSSO := cluster.Login.SSO.Enable
@@ -43,7 +43,7 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
 	case wantGoogle:
 		return &oidcLoginController{
 			Cluster:            cluster,
-			RailsProxy:         railsProxy,
+			Parent:             parent,
 			Issuer:             "https://accounts.google.com",
 			ClientID:           cluster.Login.Google.ClientID,
 			ClientSecret:       cluster.Login.Google.ClientSecret,
@@ -54,7 +54,7 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
 	case wantOpenIDConnect:
 		return &oidcLoginController{
 			Cluster:            cluster,
-			RailsProxy:         railsProxy,
+			Parent:             parent,
 			Issuer:             cluster.Login.OpenIDConnect.Issuer,
 			ClientID:           cluster.Login.OpenIDConnect.ClientID,
 			ClientSecret:       cluster.Login.OpenIDConnect.ClientSecret,
@@ -63,13 +63,13 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
 			UsernameClaim:      cluster.Login.OpenIDConnect.UsernameClaim,
 		}
 	case wantSSO:
-		return &ssoLoginController{railsProxy}
+		return &ssoLoginController{Parent: parent}
 	case wantPAM:
-		return &pamLoginController{Cluster: cluster, RailsProxy: railsProxy}
+		return &pamLoginController{Cluster: cluster, Parent: parent}
 	case wantLDAP:
-		return &ldapLoginController{Cluster: cluster, RailsProxy: railsProxy}
+		return &ldapLoginController{Cluster: cluster, Parent: parent}
 	case wantTest:
-		return &testLoginController{Cluster: cluster, RailsProxy: railsProxy}
+		return &testLoginController{Cluster: cluster, Parent: parent}
 	case wantLoginCluster:
 		return &federatedLoginController{Cluster: cluster}
 	default:
@@ -89,10 +89,16 @@ func countTrue(vals ...bool) int {
 	return n
 }
 
-// Login and Logout are passed through to the wrapped railsProxy;
+// Login and Logout are passed through to the parent's railsProxy;
 // UserAuthenticate is rejected.
-type ssoLoginController struct{ *railsProxy }
+type ssoLoginController struct{ Parent *Conn }
 
+func (ctrl *ssoLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
+	return ctrl.Parent.railsProxy.Login(ctx, opts)
+}
+func (ctrl *ssoLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+	return ctrl.Parent.railsProxy.Logout(ctx, opts)
+}
 func (ctrl *ssoLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
 	return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New("username/password authentication is not available"), http.StatusBadRequest)
 }
@@ -135,9 +141,9 @@ func noopLogout(cluster *arvados.Cluster, opts arvados.LogoutOptions) (arvados.L
 	return arvados.LogoutResponse{RedirectLocation: target}, nil
 }
 
-func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (resp arvados.APIClientAuthorization, err error) {
+func (conn *Conn) CreateAPIClientAuthorization(ctx context.Context, rootToken string, authinfo rpc.UserSessionAuthInfo) (resp arvados.APIClientAuthorization, err error) {
 	ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{rootToken}})
-	newsession, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+	newsession, err := conn.railsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
 		// Send a fake ReturnTo value instead of the caller's
 		// opts.ReturnTo. We won't follow the resulting
 		// redirect target anyway.
diff --git a/lib/controller/localdb/login_ldap.go b/lib/controller/localdb/login_ldap.go
index 6c430d69b..49f557ae5 100644
--- a/lib/controller/localdb/login_ldap.go
+++ b/lib/controller/localdb/login_ldap.go
@@ -21,8 +21,8 @@ import (
 )
 
 type ldapLoginController struct {
-	Cluster    *arvados.Cluster
-	RailsProxy *railsProxy
+	Cluster *arvados.Cluster
+	Parent  *Conn
 }
 
 func (ctrl *ldapLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
@@ -143,7 +143,7 @@ func (ctrl *ldapLoginController) UserAuthenticate(ctx context.Context, opts arva
 		return arvados.APIClientAuthorization{}, errors.New("authentication succeeded but ldap returned no email address")
 	}
 
-	return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
+	return ctrl.Parent.CreateAPIClientAuthorization(ctx, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
 		Email:     email,
 		FirstName: attrs["givenname"],
 		LastName:  attrs["sn"],
diff --git a/lib/controller/localdb/login_ldap_test.go b/lib/controller/localdb/login_ldap_test.go
index bce1ecfcf..b8ba6b467 100644
--- a/lib/controller/localdb/login_ldap_test.go
+++ b/lib/controller/localdb/login_ldap_test.go
@@ -90,8 +90,8 @@ func (s *LDAPSuite) SetUpSuite(c *check.C) {
 	s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
 	c.Assert(err, check.IsNil)
 	s.ctrl = &ldapLoginController{
-		Cluster:    s.cluster,
-		RailsProxy: railsproxy.NewConn(s.cluster),
+		Cluster: s.cluster,
+		Parent:  &Conn{railsProxy: railsproxy.NewConn(s.cluster)},
 	}
 	s.db = arvadostest.DB(c, s.cluster)
 }
diff --git a/lib/controller/localdb/login_oidc.go b/lib/controller/localdb/login_oidc.go
index 5f96da562..a41a93da1 100644
--- a/lib/controller/localdb/login_oidc.go
+++ b/lib/controller/localdb/login_oidc.go
@@ -46,7 +46,7 @@ const (
 
 type oidcLoginController struct {
 	Cluster            *arvados.Cluster
-	RailsProxy         *railsProxy
+	Parent             *Conn
 	Issuer             string // OIDC issuer URL, e.g., "https://accounts.google.com"
 	ClientID           string
 	ClientSecret       string
@@ -143,7 +143,7 @@ func (ctrl *oidcLoginController) Login(ctx context.Context, opts arvados.LoginOp
 		return loginError(err)
 	}
 	ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}})
-	return ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+	return ctrl.Parent.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
 		ReturnTo: state.Remote + "," + state.ReturnTo,
 		AuthInfo: *authinfo,
 	})
diff --git a/lib/controller/localdb/login_pam.go b/lib/controller/localdb/login_pam.go
index 2447713a2..5d116a9e8 100644
--- a/lib/controller/localdb/login_pam.go
+++ b/lib/controller/localdb/login_pam.go
@@ -20,8 +20,8 @@ import (
 )
 
 type pamLoginController struct {
-	Cluster    *arvados.Cluster
-	RailsProxy *railsProxy
+	Cluster *arvados.Cluster
+	Parent  *Conn
 }
 
 func (ctrl *pamLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
@@ -87,7 +87,7 @@ func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvad
 		"user":  user,
 		"email": email,
 	}).Debug("pam authentication succeeded")
-	return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
+	return ctrl.Parent.CreateAPIClientAuthorization(ctx, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
 		Username: user,
 		Email:    email,
 	})
diff --git a/lib/controller/localdb/login_pam_test.go b/lib/controller/localdb/login_pam_test.go
index e6b967c94..c5876bbfa 100644
--- a/lib/controller/localdb/login_pam_test.go
+++ b/lib/controller/localdb/login_pam_test.go
@@ -36,8 +36,8 @@ func (s *PamSuite) SetUpSuite(c *check.C) {
 	s.cluster.Login.PAM.DefaultEmailDomain = "example.com"
 	s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
 	s.ctrl = &pamLoginController{
-		Cluster:    s.cluster,
-		RailsProxy: rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider),
+		Cluster: s.cluster,
+		Parent:  &Conn{railsProxy: rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)},
 	}
 }
 
diff --git a/lib/controller/localdb/login_testuser.go b/lib/controller/localdb/login_testuser.go
index 585227352..c567a0668 100644
--- a/lib/controller/localdb/login_testuser.go
+++ b/lib/controller/localdb/login_testuser.go
@@ -17,8 +17,8 @@ import (
 )
 
 type testLoginController struct {
-	Cluster    *arvados.Cluster
-	RailsProxy *railsProxy
+	Cluster *arvados.Cluster
+	Parent  *Conn
 }
 
 func (ctrl *testLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
@@ -45,7 +45,7 @@ func (ctrl *testLoginController) UserAuthenticate(ctx context.Context, opts arva
 				"username": username,
 				"email":    user.Email,
 			}).Debug("test authentication succeeded")
-			return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
+			return ctrl.Parent.CreateAPIClientAuthorization(ctx, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
 				Username: username,
 				Email:    user.Email,
 			})
diff --git a/lib/controller/localdb/login_testuser_test.go b/lib/controller/localdb/login_testuser_test.go
index 758908889..7a520428b 100644
--- a/lib/controller/localdb/login_testuser_test.go
+++ b/lib/controller/localdb/login_testuser_test.go
@@ -41,8 +41,8 @@ func (s *TestUserSuite) SetUpSuite(c *check.C) {
 	}
 	s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
 	s.ctrl = &testLoginController{
-		Cluster:    s.cluster,
-		RailsProxy: rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider),
+		Cluster: s.cluster,
+		Parent:  &Conn{railsProxy: rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)},
 	}
 	s.db = arvadostest.DB(c, s.cluster)
 }
diff --git a/lib/controller/router/response.go b/lib/controller/router/response.go
index 543e25d0c..055595c8e 100644
--- a/lib/controller/router/response.go
+++ b/lib/controller/router/response.go
@@ -106,27 +106,43 @@ func (rtr *router) sendResponse(w http.ResponseWriter, req *http.Request, resp i
 		tmp = applySelectParam(opts.Select, tmp)
 	}
 
-	// Format non-nil timestamps as rfc3339NanoFixed (by default
-	// they will have been encoded to time.RFC3339Nano, which
-	// omits trailing zeroes).
 	for k, v := range tmp {
-		if !strings.HasSuffix(k, "_at") {
-			continue
+		if strings.HasSuffix(k, "_at") {
+			// Format non-nil timestamps as
+			// rfc3339NanoFixed (by default they will have
+			// been encoded to time.RFC3339Nano, which
+			// omits trailing zeroes).
+			switch tv := v.(type) {
+			case *time.Time:
+				if tv == nil {
+					break
+				}
+				tmp[k] = tv.Format(rfc3339NanoFixed)
+			case time.Time:
+				if tv.IsZero() {
+					tmp[k] = nil
+				} else {
+					tmp[k] = tv.Format(rfc3339NanoFixed)
+				}
+			case string:
+				t, err := time.Parse(time.RFC3339Nano, tv)
+				if err != nil {
+					break
+				}
+				tmp[k] = t.Format(rfc3339NanoFixed)
+			}
 		}
-		switch tv := v.(type) {
-		case *time.Time:
-			if tv == nil {
-				break
+		switch k {
+		// in all this cases, RoR returns nil instead the Zero value for the type.
+		// Maytbe, this should all go away when RoR is out of the picture.
+		case "output_uuid", "output_name", "log_uuid", "modified_by_client_uuid", "description", "requesting_container_uuid", "expires_at":
+			if v == "" {
+				tmp[k] = nil
 			}
-			tmp[k] = tv.Format(rfc3339NanoFixed)
-		case time.Time:
-			tmp[k] = tv.Format(rfc3339NanoFixed)
-		case string:
-			t, err := time.Parse(time.RFC3339Nano, tv)
-			if err != nil {
-				break
+		case "container_count_max":
+			if v == float64(0) {
+				tmp[k] = nil
 			}
-			tmp[k] = t.Format(rfc3339NanoFixed)
 		}
 	}
 	w.Header().Set("Content-Type", "application/json")
diff --git a/lib/controller/router/router.go b/lib/controller/router/router.go
index 294452434..9fb2a0d32 100644
--- a/lib/controller/router/router.go
+++ b/lib/controller/router/router.go
@@ -168,6 +168,41 @@ func (rtr *router) addRoutes() {
 				return rtr.backend.ContainerDelete(ctx, *opts.(*arvados.DeleteOptions))
 			},
 		},
+		{
+			arvados.EndpointContainerRequestCreate,
+			func() interface{} { return &arvados.CreateOptions{} },
+			func(ctx context.Context, opts interface{}) (interface{}, error) {
+				return rtr.backend.ContainerRequestCreate(ctx, *opts.(*arvados.CreateOptions))
+			},
+		},
+		{
+			arvados.EndpointContainerRequestUpdate,
+			func() interface{} { return &arvados.UpdateOptions{} },
+			func(ctx context.Context, opts interface{}) (interface{}, error) {
+				return rtr.backend.ContainerRequestUpdate(ctx, *opts.(*arvados.UpdateOptions))
+			},
+		},
+		{
+			arvados.EndpointContainerRequestGet,
+			func() interface{} { return &arvados.GetOptions{} },
+			func(ctx context.Context, opts interface{}) (interface{}, error) {
+				return rtr.backend.ContainerRequestGet(ctx, *opts.(*arvados.GetOptions))
+			},
+		},
+		{
+			arvados.EndpointContainerRequestList,
+			func() interface{} { return &arvados.ListOptions{Limit: -1} },
+			func(ctx context.Context, opts interface{}) (interface{}, error) {
+				return rtr.backend.ContainerRequestList(ctx, *opts.(*arvados.ListOptions))
+			},
+		},
+		{
+			arvados.EndpointContainerRequestDelete,
+			func() interface{} { return &arvados.DeleteOptions{} },
+			func(ctx context.Context, opts interface{}) (interface{}, error) {
+				return rtr.backend.ContainerRequestDelete(ctx, *opts.(*arvados.DeleteOptions))
+			},
+		},
 		{
 			arvados.EndpointContainerLock,
 			func() interface{} {
diff --git a/lib/controller/rpc/conn.go b/lib/controller/rpc/conn.go
index cd98b6471..5ffa66801 100644
--- a/lib/controller/rpc/conn.go
+++ b/lib/controller/rpc/conn.go
@@ -286,6 +286,41 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
 	return resp, err
 }
 
+func (conn *Conn) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
+	ep := arvados.EndpointContainerRequestCreate
+	var resp arvados.ContainerRequest
+	err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+	return resp, err
+}
+
+func (conn *Conn) ContainerRequestUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.ContainerRequest, error) {
+	ep := arvados.EndpointContainerRequestUpdate
+	var resp arvados.ContainerRequest
+	err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+	return resp, err
+}
+
+func (conn *Conn) ContainerRequestGet(ctx context.Context, options arvados.GetOptions) (arvados.ContainerRequest, error) {
+	ep := arvados.EndpointContainerRequestGet
+	var resp arvados.ContainerRequest
+	err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+	return resp, err
+}
+
+func (conn *Conn) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
+	ep := arvados.EndpointContainerRequestList
+	var resp arvados.ContainerRequestList
+	err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+	return resp, err
+}
+
+func (conn *Conn) ContainerRequestDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.ContainerRequest, error) {
+	ep := arvados.EndpointContainerRequestDelete
+	var resp arvados.ContainerRequest
+	err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+	return resp, err
+}
+
 func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
 	ep := arvados.EndpointSpecimenCreate
 	var resp arvados.Specimen
@@ -402,6 +437,7 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv
 }
 
 type UserSessionAuthInfo struct {
+	UserUUID        string   `json:"user_uuid"`
 	Email           string   `json:"email"`
 	AlternateEmails []string `json:"alternate_emails"`
 	FirstName       string   `json:"first_name"`
diff --git a/sdk/go/arvados/api.go b/sdk/go/arvados/api.go
index 5a2cfb880..a11872971 100644
--- a/sdk/go/arvados/api.go
+++ b/sdk/go/arvados/api.go
@@ -41,6 +41,11 @@ var (
 	EndpointContainerDelete               = APIEndpoint{"DELETE", "arvados/v1/containers/{uuid}", ""}
 	EndpointContainerLock                 = APIEndpoint{"POST", "arvados/v1/containers/{uuid}/lock", ""}
 	EndpointContainerUnlock               = APIEndpoint{"POST", "arvados/v1/containers/{uuid}/unlock", ""}
+	EndpointContainerRequestCreate        = APIEndpoint{"POST", "arvados/v1/container_requests", "container_request"}
+	EndpointContainerRequestUpdate        = APIEndpoint{"PATCH", "arvados/v1/container_requests/{uuid}", "container_request"}
+	EndpointContainerRequestGet           = APIEndpoint{"GET", "arvados/v1/container_requests/{uuid}", ""}
+	EndpointContainerRequestList          = APIEndpoint{"GET", "arvados/v1/container_requests", ""}
+	EndpointContainerRequestDelete        = APIEndpoint{"DELETE", "arvados/v1/container_requests/{uuid}", ""}
 	EndpointUserActivate                  = APIEndpoint{"POST", "arvados/v1/users/{uuid}/activate", ""}
 	EndpointUserCreate                    = APIEndpoint{"POST", "arvados/v1/users", "user"}
 	EndpointUserCurrent                   = APIEndpoint{"GET", "arvados/v1/users/current", ""}
@@ -175,6 +180,11 @@ type API interface {
 	ContainerDelete(ctx context.Context, options DeleteOptions) (Container, error)
 	ContainerLock(ctx context.Context, options GetOptions) (Container, error)
 	ContainerUnlock(ctx context.Context, options GetOptions) (Container, error)
+	ContainerRequestCreate(ctx context.Context, options CreateOptions) (ContainerRequest, error)
+	ContainerRequestUpdate(ctx context.Context, options UpdateOptions) (ContainerRequest, error)
+	ContainerRequestGet(ctx context.Context, options GetOptions) (ContainerRequest, error)
+	ContainerRequestList(ctx context.Context, options ListOptions) (ContainerRequestList, error)
+	ContainerRequestDelete(ctx context.Context, options DeleteOptions) (ContainerRequest, error)
 	SpecimenCreate(ctx context.Context, options CreateOptions) (Specimen, error)
 	SpecimenUpdate(ctx context.Context, options UpdateOptions) (Specimen, error)
 	SpecimenGet(ctx context.Context, options GetOptions) (Specimen, error)
diff --git a/sdk/go/arvados/container.go b/sdk/go/arvados/container.go
index 265944e81..433db3813 100644
--- a/sdk/go/arvados/container.go
+++ b/sdk/go/arvados/container.go
@@ -48,7 +48,7 @@ type ContainerRequest struct {
 	Properties              map[string]interface{} `json:"properties"`
 	State                   ContainerRequestState  `json:"state"`
 	RequestingContainerUUID string                 `json:"requesting_container_uuid"`
-	ContainerUUID           string                 `json:"container_uuid"`
+	ContainerUUID           *string                `json:"container_uuid"`
 	ContainerCountMax       int                    `json:"container_count_max"`
 	Mounts                  map[string]Mount       `json:"mounts"`
 	RuntimeConstraints      RuntimeConstraints     `json:"runtime_constraints"`
@@ -65,6 +65,9 @@ type ContainerRequest struct {
 	LogUUID                 string                 `json:"log_uuid"`
 	OutputUUID              string                 `json:"output_uuid"`
 	RuntimeToken            string                 `json:"runtime_token"`
+	ExpiresAt               string                 `json:"expires_at"`
+	Filters                 []string               `json:"filters"`
+	ContainerCount          int                    `json:"container_count"`
 }
 
 // Mount is special behavior to attach to a filesystem path or device.
@@ -86,18 +89,18 @@ type Mount struct {
 // RuntimeConstraints specify a container's compute resources (RAM,
 // CPU) and network connectivity.
 type RuntimeConstraints struct {
-	API          *bool
-	RAM          int64 `json:"ram"`
-	VCPUs        int   `json:"vcpus"`
-	KeepCacheRAM int64 `json:"keep_cache_ram"`
+	API          *bool `json:",omitempty"`
+	RAM          int64 `json:"ram,omitempty"`
+	VCPUs        int   `json:"vcpus,omitempty"`
+	KeepCacheRAM int64 `json:"keep_cache_ram,omitempty"`
 }
 
 // SchedulingParameters specify a container's scheduling parameters
 // such as Partitions
 type SchedulingParameters struct {
-	Partitions  []string `json:"partitions"`
-	Preemptible bool     `json:"preemptible"`
-	MaxRunTime  int      `json:"max_run_time"`
+	Partitions  []string `json:"partitions,omitempty"`
+	Preemptible bool     `json:"preemptible,omitempty"`
+	MaxRunTime  int      `json:"max_run_time,omitempty"`
 }
 
 // ContainerList is an arvados#containerList resource.
diff --git a/sdk/go/arvadostest/api.go b/sdk/go/arvadostest/api.go
index fa5f53936..ce0a913b5 100644
--- a/sdk/go/arvadostest/api.go
+++ b/sdk/go/arvadostest/api.go
@@ -105,6 +105,26 @@ func (as *APIStub) ContainerUnlock(ctx context.Context, options arvados.GetOptio
 	as.appendCall(as.ContainerUnlock, ctx, options)
 	return arvados.Container{}, as.Error
 }
+func (as *APIStub) ContainerRequestCreate(ctx context.Context, options arvados.CreateOptions) (arvados.ContainerRequest, error) {
+	as.appendCall(as.ContainerRequestCreate, ctx, options)
+	return arvados.ContainerRequest{}, as.Error
+}
+func (as *APIStub) ContainerRequestUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.ContainerRequest, error) {
+	as.appendCall(as.ContainerRequestUpdate, ctx, options)
+	return arvados.ContainerRequest{}, as.Error
+}
+func (as *APIStub) ContainerRequestGet(ctx context.Context, options arvados.GetOptions) (arvados.ContainerRequest, error) {
+	as.appendCall(as.ContainerRequestGet, ctx, options)
+	return arvados.ContainerRequest{}, as.Error
+}
+func (as *APIStub) ContainerRequestList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerRequestList, error) {
+	as.appendCall(as.ContainerRequestList, ctx, options)
+	return arvados.ContainerRequestList{}, as.Error
+}
+func (as *APIStub) ContainerRequestDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.ContainerRequest, error) {
+	as.appendCall(as.ContainerRequestDelete, ctx, options)
+	return arvados.ContainerRequest{}, as.Error
+}
 func (as *APIStub) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
 	as.appendCall(as.SpecimenCreate, ctx, options)
 	return arvados.Specimen{}, as.Error
diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index 8e3c3ac5e..0bf939d08 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -30,12 +30,20 @@ class UserSessionsController < ApplicationController
       authinfo = request.env['omniauth.auth']['info'].with_indifferent_access
     end
 
-    begin
-      user = User.register(authinfo)
-    rescue => e
-      Rails.logger.warn "User.register error #{e}"
-      Rails.logger.warn "authinfo was #{authinfo.inspect}"
-      return redirect_to login_failure_url
+    if !authinfo['user_uuid'].blank?
+      user = User.find_by_uuid(authinfo['user_uuid'])
+      if !user
+        Rails.logger.warn "Nonexistent user_uuid in authinfo #{authinfo.inspect}"
+        return redirect_to login_failure_url
+      end
+    else
+      begin
+        user = User.register(authinfo)
+      rescue => e
+        Rails.logger.warn "User.register error #{e}"
+        Rails.logger.warn "authinfo was #{authinfo.inspect}"
+        return redirect_to login_failure_url
+      end
     end
 
     # For the benefit of functional and integration tests:
diff --git a/services/api/test/fixtures/container_requests.yml b/services/api/test/fixtures/container_requests.yml
index ea86dca17..daaaf4d08 100644
--- a/services/api/test/fixtures/container_requests.yml
+++ b/services/api/test/fixtures/container_requests.yml
@@ -20,6 +20,7 @@ queued:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 running:
   uuid: zzzzz-xvhdp-cr4runningcntnr
@@ -39,6 +40,7 @@ running:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 requester_for_running:
   uuid: zzzzz-xvhdp-req4runningcntr
@@ -59,6 +61,7 @@ requester_for_running:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 running_older:
   uuid: zzzzz-xvhdp-cr4runningcntn2
@@ -78,6 +81,7 @@ running_older:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 completed:
   uuid: zzzzz-xvhdp-cr4completedctr
@@ -99,6 +103,7 @@ completed:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 completed-older:
   uuid: zzzzz-xvhdp-cr4completedcr2
@@ -118,6 +123,7 @@ completed-older:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 requester:
   uuid: zzzzz-xvhdp-9zacv3o1xw6sxz5
@@ -137,6 +143,7 @@ requester:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 cr_for_requester:
   uuid: zzzzz-xvhdp-cr4requestercnt
@@ -157,6 +164,7 @@ cr_for_requester:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 cr_for_requester2:
   uuid: zzzzz-xvhdp-cr4requestercn2
@@ -176,6 +184,7 @@ cr_for_requester2:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 running_anonymous_accessible:
   uuid: zzzzz-xvhdp-runninganonaccs
@@ -195,6 +204,7 @@ running_anonymous_accessible:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 cr_for_failed:
   uuid: zzzzz-xvhdp-cr4failedcontnr
@@ -214,6 +224,7 @@ cr_for_failed:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 canceled_with_queued_container:
   uuid: zzzzz-xvhdp-canceledqueuedc
@@ -233,6 +244,7 @@ canceled_with_queued_container:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 canceled_with_locked_container:
   uuid: zzzzz-xvhdp-canceledlocekdc
@@ -252,6 +264,7 @@ canceled_with_locked_container:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 canceled_with_running_container:
   uuid: zzzzz-xvhdp-canceledrunning
@@ -271,6 +284,7 @@ canceled_with_running_container:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 running_to_be_deleted:
   uuid: zzzzz-xvhdp-cr5runningcntnr
@@ -290,6 +304,7 @@ running_to_be_deleted:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 completed_with_input_mounts:
   uuid: zzzzz-xvhdp-crwithinputmnts
@@ -311,18 +326,23 @@ completed_with_input_mounts:
   container_uuid: zzzzz-dz642-compltcontainer
   log_uuid: zzzzz-4zz18-y9vne9npefyxh8g
   output_uuid: zzzzz-4zz18-znfnqtbbv4spc3w
-  mounts:
-    /var/lib/cwl/cwl.input.json:
-      content:
-        input1:
-          basename: foo
-          class: File
-          location: "keep:fa7aeb5140e2848d39b416daeef4ffc5+45/foo"
-        input2:
-          basename: bar
-          class: File
-          location: "keep:fa7aeb5140e2848d39b416daeef4ffc5+45/bar"
-    /var/lib/cwl/workflow.json: "keep:f9ddda46bb293b6847da984e3aa735db+290"
+  mounts: {
+    "/var/lib/cwl/cwl.input.json": {
+      "kind": "json",
+      "content": {
+        "input1": {
+          "basename": "foo",
+          "class": "File",
+          "location": "keep:fa7aeb5140e2848d39b416daeef4ffc5+45/foo",
+        },
+        "input2": {
+          "basename": "bar",
+          "class": "File",
+          "location": "keep:fa7aeb5140e2848d39b416daeef4ffc5+45/bar",
+        }
+      }
+    }
+  }
 
 uncommitted:
   uuid: zzzzz-xvhdp-cr4uncommittedc
@@ -763,6 +783,7 @@ cr_in_trashed_project:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 runtime_token:
   uuid: zzzzz-xvhdp-11eklkhy0n4dm86
@@ -783,6 +804,7 @@ runtime_token:
   runtime_constraints:
     vcpus: 1
     ram: 123
+  mounts: {}
 
 
 # Test Helper trims the rest of the file
@@ -798,6 +820,7 @@ cr_<%=i%>_of_60:
   name: cr-<%= i.to_s %>
   output_path: test
   command: ["echo", "hello"]
+  mounts: {}
 <% end %>
 
 # Do not add your fixtures below this line as the rest of this file will be trimmed by test_helper

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list