15720: Merge branch 'master' into 15720-fed-user-list
authorTom Clegg <tclegg@veritasgenetics.com>
Mon, 25 Nov 2019 20:20:21 +0000 (15:20 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Mon, 25 Nov 2019 20:20:21 +0000 (15:20 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

1  2 
lib/controller/federation/conn.go
lib/controller/federation/generated.go
lib/controller/federation/list.go
lib/controller/federation/list_test.go
lib/controller/federation/login_test.go
lib/controller/router/router_test.go
lib/controller/rpc/conn.go

index 1d8fa7e462cf96480adba03d7d814b1c246911c6,3a439eb7d4c2d9339cbdc615a52a06d0d8dce4cd..887102f8e58f4d659d3ed4c52b95d92a8e460003
@@@ -15,7 -15,6 +15,7 @@@ import 
        "net/url"
        "regexp"
        "strings"
 +      "time"
  
        "git.curoverse.com/arvados.git/lib/config"
        "git.curoverse.com/arvados.git/lib/controller/localdb"
@@@ -198,10 -197,13 +198,13 @@@ func (conn *Conn) Login(ctx context.Con
                if err != nil {
                        return arvados.LoginResponse{}, fmt.Errorf("internal error getting redirect target: %s", err)
                }
-               target.RawQuery = url.Values{
+               params := url.Values{
                        "return_to": []string{options.ReturnTo},
-                       "remote":    []string{options.Remote},
-               }.Encode()
+               }
+               if options.Remote != "" {
+                       params.Set("remote", options.Remote)
+               }
+               target.RawQuery = params.Encode()
                return arvados.LoginResponse{
                        RedirectLocation: target.String(),
                }, nil
@@@ -252,10 -254,6 +255,10 @@@ func (conn *Conn) CollectionGet(ctx con
        }
  }
  
 +func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
 +      return conn.generated_CollectionList(ctx, options)
 +}
 +
  func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) {
        return conn.chooseBackend(options.UUID).CollectionProvenance(ctx, options)
  }
@@@ -276,10 -274,6 +279,10 @@@ func (conn *Conn) CollectionUntrash(ct
        return conn.chooseBackend(options.UUID).CollectionUntrash(ctx, options)
  }
  
 +func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
 +      return conn.generated_ContainerList(ctx, options)
 +}
 +
  func (conn *Conn) ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) {
        return conn.chooseBackend(options.ClusterID).ContainerCreate(ctx, options)
  }
@@@ -304,10 -298,6 +307,10 @@@ func (conn *Conn) ContainerUnlock(ctx c
        return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
  }
  
 +func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
 +      return conn.generated_SpecimenList(ctx, options)
 +}
 +
  func (conn *Conn) SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) {
        return conn.chooseBackend(options.ClusterID).SpecimenCreate(ctx, options)
  }
@@@ -324,139 -314,6 +327,139 @@@ func (conn *Conn) SpecimenDelete(ctx co
        return conn.chooseBackend(options.UUID).SpecimenDelete(ctx, options)
  }
  
 +var userAttrsCachedFromLoginCluster = map[string]bool{
 +      "created_at":              true,
 +      "email":                   true,
 +      "first_name":              true,
 +      "is_active":               true,
 +      "is_admin":                true,
 +      "last_name":               true,
 +      "modified_at":             true,
 +      "modified_by_client_uuid": true,
 +      "modified_by_user_uuid":   true,
 +      "prefs":                   true,
 +      "username":                true,
 +
 +      "full_name":    false,
 +      "identity_url": false,
 +      "is_invited":   false,
 +      "owner_uuid":   false,
 +      "uuid":         false,
 +}
 +
 +func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
 +      logger := ctxlog.FromContext(ctx)
 +      if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID {
 +              resp, err := conn.chooseBackend(id).UserList(ctx, options)
 +              if err != nil {
 +                      return resp, err
 +              }
 +              batchOpts := arvados.UserBatchUpdateOptions{Updates: map[string]map[string]interface{}{}}
 +              for _, user := range resp.Items {
 +                      if !strings.HasPrefix(user.UUID, id) {
 +                              continue
 +                      }
 +                      logger.Debugf("cache user info for uuid %q", user.UUID)
 +
 +                      // If the remote cluster has null timestamps
 +                      // (e.g., test server with incomplete
 +                      // fixtures) use dummy timestamps (instead of
 +                      // the zero time, which causes a Rails API
 +                      // error "year too big to marshal: 1 UTC").
 +                      if user.ModifiedAt.IsZero() {
 +                              user.ModifiedAt = time.Now()
 +                      }
 +                      if user.CreatedAt.IsZero() {
 +                              user.CreatedAt = time.Now()
 +                      }
 +
 +                      var allFields map[string]interface{}
 +                      buf, err := json.Marshal(user)
 +                      if err != nil {
 +                              return arvados.UserList{}, fmt.Errorf("error encoding user record from remote response: %s", err)
 +                      }
 +                      err = json.Unmarshal(buf, &allFields)
 +                      if err != nil {
 +                              return arvados.UserList{}, fmt.Errorf("error transcoding user record from remote response: %s", err)
 +                      }
 +                      updates := allFields
 +                      if len(options.Select) > 0 {
 +                              updates = map[string]interface{}{}
 +                              for _, k := range options.Select {
 +                                      if v, ok := allFields[k]; ok && userAttrsCachedFromLoginCluster[k] {
 +                                              updates[k] = v
 +                                      }
 +                              }
 +                      } else {
 +                              for k := range updates {
 +                                      if !userAttrsCachedFromLoginCluster[k] {
 +                                              delete(updates, k)
 +                                      }
 +                              }
 +                      }
 +                      batchOpts.Updates[user.UUID] = updates
 +              }
 +              if len(batchOpts.Updates) > 0 {
 +                      ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
 +                      _, err = conn.local.UserBatchUpdate(ctxRoot, batchOpts)
 +                      if err != nil {
 +                              return arvados.UserList{}, fmt.Errorf("error updating local user records: %s", err)
 +                      }
 +              }
 +              return resp, nil
 +      } else {
 +              return conn.generated_UserList(ctx, options)
 +      }
 +}
 +
 +func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.ClusterID).UserCreate(ctx, options)
 +}
 +
 +func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserUpdate(ctx, options)
 +}
 +
 +func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserUpdateUUID(ctx, options)
 +}
 +
 +func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.OldUserUUID).UserMerge(ctx, options)
 +}
 +
 +func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserActivate(ctx, options)
 +}
 +
 +func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
 +      return conn.chooseBackend(options.UUID).UserSetup(ctx, options)
 +}
 +
 +func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserUnsetup(ctx, options)
 +}
 +
 +func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserGet(ctx, options)
 +}
 +
 +func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserGetCurrent(ctx, options)
 +}
 +
 +func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserGetSystem(ctx, options)
 +}
 +
 +func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
 +      return conn.chooseBackend(options.UUID).UserDelete(ctx, options)
 +}
 +
 +func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
 +      return conn.local.UserBatchUpdate(ctx, options)
 +}
 +
  func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
        return conn.chooseBackend(options.UUID).APIClientAuthorizationCurrent(ctx, options)
  }
index 961cd5a401e16b0bdda33507496b1b6a2ef5b7b9,fb91a84960547d6dc6099e2e7e3ca69fb162afd8..0a66644985662596c06f2d706035a8b2097a99de
@@@ -8,6 -8,7 +8,7 @@@ import 
        "context"
        "sort"
        "sync"
+       "sync/atomic"
  
        "git.curoverse.com/arvados.git/sdk/go/arvados"
  )
  // -- this file is auto-generated -- do not edit -- edit list.go and run "go generate" instead --
  //
  
 -func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
 +func (conn *Conn) generated_ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) {
        var mtx sync.Mutex
        var merged arvados.ContainerList
+       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) {
                cl, err := backend.ContainerList(ctx, options)
                if err != nil {
@@@ -28,8 -31,9 +31,9 @@@
                defer mtx.Unlock()
                if len(merged.Items) == 0 {
                        merged = cl
-               } else {
+               } 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 {
                }
                return uuids, nil
        })
-       sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
+       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.Container{}
+       }
        return merged, err
  }
  
 -func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
 +func (conn *Conn) generated_SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) {
        var mtx sync.Mutex
        var merged arvados.SpecimenList
+       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) {
                cl, err := backend.SpecimenList(ctx, options)
                if err != nil {
@@@ -53,8 -71,9 +71,9 @@@
                defer mtx.Unlock()
                if len(merged.Items) == 0 {
                        merged = cl
-               } else {
+               } 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 {
                }
                return uuids, nil
        })
-       sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
+       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.Specimen{}
+       }
        return merged, err
  }
 +
 +func (conn *Conn) generated_UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
 +      var mtx sync.Mutex
 +      var merged arvados.UserList
 +      err := conn.splitListRequest(ctx, options, func(ctx context.Context, _ string, backend arvados.API, options arvados.ListOptions) ([]string, error) {
 +              cl, err := backend.UserList(ctx, options)
 +              if err != nil {
 +                      return nil, err
 +              }
 +              mtx.Lock()
 +              defer mtx.Unlock()
 +              if len(merged.Items) == 0 {
 +                      merged = cl
 +              } else {
 +                      merged.Items = append(merged.Items, cl.Items...)
 +              }
 +              uuids := make([]string, 0, len(cl.Items))
 +              for _, item := range cl.Items {
 +                      uuids = append(uuids, item.UUID)
 +              }
 +              return uuids, nil
 +      })
 +      sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
 +      return merged, err
 +}
index 7178d7b0aff630bc28834af3042129b4a1e9983b,54f59812a0bceaf3706dc35bdf599d8d58a8f7be..26b6b254e8e9fbdbb59638ca441412ade5575abb
@@@ -10,6 -10,7 +10,7 @@@ import 
        "net/http"
        "sort"
        "sync"
+       "sync/atomic"
  
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
  // CollectionList is used as a template to auto-generate List()
  // methods for other types; see generate.go.
  
 -func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
 +func (conn *Conn) generated_CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) {
        var mtx sync.Mutex
        var merged arvados.CollectionList
+       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) {
                cl, err := backend.CollectionList(ctx, options)
                if err != nil {
@@@ -32,8 -35,9 +35,9 @@@
                defer mtx.Unlock()
                if len(merged.Items) == 0 {
                        merged = cl
-               } else {
+               } 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 {
                }
                return uuids, nil
        })
-       sort.Slice(merged.Items, func(i, j int) bool { return merged.Items[i].UUID < merged.Items[j].UUID })
+       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.Collection{}
+       }
        return merged, err
  }
  
index 5a630a9450ef861ed7c91c5f001f3fa642378638,35d201028b96e3a2a0c3033e4a5d06fe68f532ff..a9c4f588f12a38b017d1c89b2995263a8c12d3d6
@@@ -8,13 -8,75 +8,14 @@@ import 
        "context"
        "fmt"
        "net/http"
 -      "net/url"
 -      "os"
+       "sort"
 -      "testing"
  
 -      "git.curoverse.com/arvados.git/lib/controller/router"
 -      "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
 -      "git.curoverse.com/arvados.git/sdk/go/auth"
 -      "git.curoverse.com/arvados.git/sdk/go/ctxlog"
 -      "git.curoverse.com/arvados.git/sdk/go/httpserver"
        check "gopkg.in/check.v1"
  )
  
 -// Gocheck boilerplate
 -func Test(t *testing.T) {
 -      check.TestingT(t)
 -}
 -
 -var (
 -      _ = check.Suite(&FederationSuite{})
 -      _ = check.Suite(&CollectionListSuite{})
 -)
 -
 -type FederationSuite struct {
 -      cluster *arvados.Cluster
 -      ctx     context.Context
 -      fed     *Conn
 -}
 -
 -func (s *FederationSuite) SetUpTest(c *check.C) {
 -      s.cluster = &arvados.Cluster{
 -              ClusterID: "aaaaa",
 -              RemoteClusters: map[string]arvados.RemoteCluster{
 -                      "aaaaa": arvados.RemoteCluster{
 -                              Host: os.Getenv("ARVADOS_API_HOST"),
 -                      },
 -              },
 -      }
 -      arvadostest.SetServiceURL(&s.cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
 -      s.cluster.TLS.Insecure = true
 -      s.cluster.API.MaxItemsPerResponse = 3
 -
 -      ctx := context.Background()
 -      ctx = ctxlog.Context(ctx, ctxlog.TestLogger(c))
 -      ctx = auth.NewContext(ctx, &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
 -      s.ctx = ctx
 -
 -      s.fed = New(s.cluster)
 -}
 -
 -func (s *FederationSuite) addDirectRemote(c *check.C, id string, backend backend) {
 -      s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
 -              Host: "in-process.local",
 -      }
 -      s.fed.remotes[id] = backend
 -}
 -
 -func (s *FederationSuite) addHTTPRemote(c *check.C, id string, backend backend) {
 -      srv := httpserver.Server{Addr: ":"}
 -      srv.Handler = router.New(backend)
 -      c.Check(srv.Start(), check.IsNil)
 -      s.cluster.RemoteClusters[id] = arvados.RemoteCluster{
 -              Host:  srv.Addr,
 -              Proxy: true,
 -      }
 -      s.fed.remotes[id] = rpc.NewConn(id, &url.URL{Scheme: "http", Host: srv.Addr}, true, saltedTokenProvider(s.fed.local, id))
 -}
 +var _ = check.Suite(&CollectionListSuite{})
  
  type collectionLister struct {
        arvadostest.APIStub
@@@ -365,10 -427,13 +366,13 @@@ func (s *CollectionListSuite) test(c *c
                c.Logf("returned error string is %q", err)
        } else {
                c.Check(err, check.IsNil)
-               var expectItems []arvados.Collection
+               expectItems := []arvados.Collection{}
                for _, uuid := range trial.expectUUIDs {
                        expectItems = append(expectItems, arvados.Collection{UUID: uuid})
                }
+               // expectItems is sorted by UUID, so sort resp.Items
+               // by UUID before checking DeepEquals.
+               sort.Slice(resp.Items, func(i, j int) bool { return resp.Items[i].UUID < resp.Items[j].UUID })
                c.Check(resp, check.DeepEquals, arvados.CollectionList{
                        Items: expectItems,
                })
index f83f5fb9359bd0765a84dc86cde1f94b35ab9655,e294df7d89f5e39d2467544ee49623e4ecda5fcc..8ec2bd5a4910db98d04d5d042453371d78455870
@@@ -13,13 -13,7 +13,13 @@@ import 
        check "gopkg.in/check.v1"
  )
  
 -func (s *FederationSuite) TestDeferToLoginCluster(c *check.C) {
 +var _ = check.Suite(&LoginSuite{})
 +
 +type LoginSuite struct {
 +      FederationSuite
 +}
 +
 +func (s *LoginSuite) TestDeferToLoginCluster(c *check.C) {
        s.addHTTPRemote(c, "zhome", &arvadostest.APIStub{})
        s.cluster.Login.LoginCluster = "zhome"
  
@@@ -32,7 -26,9 +32,9 @@@
                c.Check(err, check.IsNil)
                c.Check(target.Host, check.Equals, s.cluster.RemoteClusters["zhome"].Host)
                c.Check(target.Scheme, check.Equals, "http")
-               c.Check(target.Query().Get("remote"), check.Equals, remote)
                c.Check(target.Query().Get("return_to"), check.Equals, returnTo)
+               c.Check(target.Query().Get("remote"), check.Equals, remote)
+               _, remotePresent := target.Query()["remote"]
+               c.Check(remotePresent, check.Equals, remote != "")
        }
  }
index 6a9fd311ba2b91cd66b03aa4dc7013eb878ea665,a42df278f43043ee7eeb4621d5ecaa2faa0cb275..b1bc9bce32b202548942dd3869f3ed2073ddaa85
@@@ -19,7 -19,7 +19,7 @@@ import 
        "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
 -      "github.com/julienschmidt/httprouter"
 +      "github.com/gorilla/mux"
        check "gopkg.in/check.v1"
  )
  
@@@ -38,7 -38,7 +38,7 @@@ type RouterSuite struct 
  func (s *RouterSuite) SetUpTest(c *check.C) {
        s.stub = arvadostest.APIStub{}
        s.rtr = &router{
 -              mux: httprouter.New(),
 +              mux: mux.NewRouter(),
                fed: &s.stub,
        }
        s.rtr.addRoutes()
@@@ -225,6 -225,13 +225,13 @@@ func (s *RouterIntegrationSuite) TestCo
        c.Check(rr.Code, check.Equals, http.StatusOK)
        c.Check(jresp["items_available"], check.FitsTypeOf, float64(0))
        c.Check(jresp["items_available"].(float64) > 2, check.Equals, true)
+       c.Check(jresp["items"], check.NotNil)
+       c.Check(jresp["items"], check.HasLen, 0)
+       _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers?filters=[["uuid","in",[]]]`, nil, nil)
+       c.Check(rr.Code, check.Equals, http.StatusOK)
+       c.Check(jresp["items_available"], check.Equals, float64(0))
+       c.Check(jresp["items"], check.NotNil)
        c.Check(jresp["items"], check.HasLen, 0)
  
        _, rr, jresp = doRequest(c, s.rtr, token, "GET", `/arvados/v1/containers?limit=2&select=["uuid","command"]`, nil, nil)
index 66523e5ac3203215de84795f76db632083451372,3d6a9852089c3005a084e61290da0b45f0a67489..f4bc1733eaff7112caf4bdfc7c69f62ddae5c4a8
@@@ -118,9 -118,9 +118,9 @@@ func (conn *Conn) requestAndDecode(ctx 
                params["reader_tokens"] = tokens[1:]
        }
        path := ep.Path
 -      if strings.Contains(ep.Path, "/:uuid") {
 +      if strings.Contains(ep.Path, "/{uuid}") {
                uuid, _ := params["uuid"].(string)
 -              path = strings.Replace(path, "/:uuid", "/"+uuid, 1)
 +              path = strings.Replace(path, "/{uuid}", "/"+uuid, 1)
                delete(params, "uuid")
        }
        return aClient.RequestAndDecodeContext(ctx, dst, ep.Method, path, body, params)
@@@ -308,79 -308,6 +308,79 @@@ func (conn *Conn) SpecimenDelete(ctx co
        return resp, err
  }
  
 +func (conn *Conn) UserCreate(ctx context.Context, options arvados.CreateOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserCreate
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserUpdate
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserUpdateUUID(ctx context.Context, options arvados.UpdateUUIDOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserUpdateUUID
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserUpdateUUID
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserUpdateUUID
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
 +      ep := arvados.EndpointUserUpdateUUID
 +      var resp map[string]interface{}
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserUpdateUUID
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserGet
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserGetCurrent(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserGetCurrent
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserGetSystem(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserGetSystem
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserList(ctx context.Context, options arvados.ListOptions) (arvados.UserList, error) {
 +      ep := arvados.EndpointUserList
 +      var resp arvados.UserList
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +func (conn *Conn) UserDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.User, error) {
 +      ep := arvados.EndpointUserDelete
 +      var resp arvados.User
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}
 +
  func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) {
        ep := arvados.EndpointAPIClientAuthorizationCurrent
        var resp arvados.APIClientAuthorization
@@@ -393,6 -320,7 +393,7 @@@ type UserSessionAuthInfo struct 
        AlternateEmails []string `json:"alternate_emails"`
        FirstName       string   `json:"first_name"`
        LastName        string   `json:"last_name"`
+       Username        string   `json:"username"`
  }
  
  type UserSessionCreateOptions struct {
@@@ -406,10 -334,3 +407,10 @@@ func (conn *Conn) UserSessionCreate(ct
        err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
        return resp, err
  }
 +
 +func (conn *Conn) UserBatchUpdate(ctx context.Context, options arvados.UserBatchUpdateOptions) (arvados.UserList, error) {
 +      ep := arvados.APIEndpoint{Method: "PATCH", Path: "arvados/v1/users/batch_update"}
 +      var resp arvados.UserList
 +      err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
 +      return resp, err
 +}