X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/783f215608cc94f1dd7450e802ecf8b4b06f1cf9..64e387b2f4f0fe6c4c7bf16232706c7cf194caf0:/lib/controller/federation/conn.go diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go index 0e9b285f94..42083cb83d 100644 --- a/lib/controller/federation/conn.go +++ b/lib/controller/federation/conn.go @@ -5,61 +5,44 @@ package federation import ( + "bytes" "context" "crypto/md5" + "encoding/json" "errors" "fmt" "net/http" "net/url" "regexp" "strings" - - "git.curoverse.com/arvados.git/lib/controller/railsproxy" - "git.curoverse.com/arvados.git/lib/controller/rpc" - "git.curoverse.com/arvados.git/sdk/go/arvados" - "git.curoverse.com/arvados.git/sdk/go/auth" - "git.curoverse.com/arvados.git/sdk/go/ctxlog" + "time" + + "git.arvados.org/arvados.git/lib/config" + "git.arvados.org/arvados.git/lib/controller/localdb" + "git.arvados.org/arvados.git/lib/controller/rpc" + "git.arvados.org/arvados.git/sdk/go/arvados" + "git.arvados.org/arvados.git/sdk/go/auth" + "git.arvados.org/arvados.git/sdk/go/ctxlog" ) -type Interface interface { - CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) - CollectionUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Collection, error) - CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) - CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) - CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) - CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) - CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) - CollectionTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) - CollectionUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Collection, error) - ContainerCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Container, error) - ContainerUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Container, error) - ContainerGet(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) - ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) - ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) - ContainerLock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) - ContainerUnlock(ctx context.Context, options arvados.GetOptions) (arvados.Container, error) - SpecimenCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Specimen, error) - SpecimenUpdate(ctx context.Context, options arvados.UpdateOptions) (arvados.Specimen, error) - SpecimenGet(ctx context.Context, options arvados.GetOptions) (arvados.Specimen, error) - SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) - SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) - APIClientAuthorizationCurrent(ctx context.Context, options arvados.GetOptions) (arvados.APIClientAuthorization, error) -} - type Conn struct { cluster *arvados.Cluster local backend remotes map[string]backend } -func New(cluster *arvados.Cluster) Interface { - local := railsproxy.NewConn(cluster) +func New(cluster *arvados.Cluster) *Conn { + local := localdb.NewConn(cluster) remotes := map[string]backend{} for id, remote := range cluster.RemoteClusters { if !remote.Proxy { continue } - remotes[id] = rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id)) + conn := rpc.NewConn(id, &url.URL{Scheme: remote.Scheme, Host: remote.Host}, remote.Insecure, saltedTokenProvider(local, id)) + // Older versions of controller rely on the Via header + // to detect loops. + conn.SendHeader = http.Header{"Via": {"HTTP/1.1 arvados-controller"}} + remotes[id] = conn } return &Conn{ @@ -76,7 +59,7 @@ func New(cluster *arvados.Cluster) Interface { func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider { return func(ctx context.Context) ([]string, error) { var tokens []string - incoming, ok := ctx.Value(auth.ContextKeyCredentials).(*auth.Credentials) + incoming, ok := auth.FromContext(ctx) if !ok { return nil, errors.New("no token provided") } @@ -88,7 +71,7 @@ func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider { case auth.ErrSalted: tokens = append(tokens, token) case auth.ErrObsoleteToken: - ctx := context.WithValue(ctx, auth.ContextKeyCredentials, &auth.Credentials{Tokens: []string{token}}) + ctx := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{token}}) aca, err := local.APIClientAuthorizationCurrent(ctx, arvados.GetOptions{}) if errStatus(err) == http.StatusUnauthorized { // pass through unmodified @@ -113,8 +96,11 @@ func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider { // Return suitable backend for a query about the given cluster ID // ("aaaaa") or object UUID ("aaaaa-dz642-abcdefghijklmno"). func (conn *Conn) chooseBackend(id string) backend { - if len(id) > 5 { + if len(id) == 27 { id = id[:5] + } else if len(id) != 5 { + // PDH or bogus ID + return conn.local } if id == conn.cluster.ClusterID { return conn.local @@ -134,8 +120,13 @@ func (conn *Conn) chooseBackend(id string) backend { // or "" for the local backend. // // A non-nil error means all backends failed. -func (conn *Conn) tryLocalThenRemotes(ctx context.Context, fn func(context.Context, string, backend) error) error { - if err := fn(ctx, "", conn.local); err == nil || errStatus(err) != http.StatusNotFound { +func (conn *Conn) tryLocalThenRemotes(ctx context.Context, forwardedFor string, fn func(context.Context, string, backend) error) error { + if err := fn(ctx, "", conn.local); err == nil || errStatus(err) != http.StatusNotFound || forwardedFor != "" { + // Note: forwardedFor != "" means this request came + // from a remote cluster, so we don't take a second + // hop. This avoids cycles, redundant calls to a + // mutually reachable remote, and use of double-salted + // tokens. return err } @@ -161,8 +152,7 @@ func (conn *Conn) tryLocalThenRemotes(ctx context.Context, fn func(context.Conte if all404 { return notFoundError{} } - // FIXME: choose appropriate HTTP status - return fmt.Errorf("errors: %v", errs) + return httpErrorf(http.StatusBadGateway, "errors: %v", errs) } func (conn *Conn) CollectionCreate(ctx context.Context, options arvados.CreateOptions) (arvados.Collection, error) { @@ -199,6 +189,39 @@ func portableDataHash(mt string) string { return fmt.Sprintf("%x+%d", h.Sum(nil), size) } +func (conn *Conn) ConfigGet(ctx context.Context) (json.RawMessage, error) { + var buf bytes.Buffer + err := config.ExportJSON(&buf, conn.cluster) + return json.RawMessage(buf.Bytes()), err +} + +func (conn *Conn) Login(ctx context.Context, options arvados.LoginOptions) (arvados.LoginResponse, error) { + if id := conn.cluster.Login.LoginCluster; id != "" && id != conn.cluster.ClusterID { + // defer entire login procedure to designated cluster + remote, ok := conn.remotes[id] + if !ok { + return arvados.LoginResponse{}, fmt.Errorf("configuration problem: designated login cluster %q is not defined", id) + } + baseURL := remote.BaseURL() + target, err := baseURL.Parse(arvados.EndpointLogin.Path) + if err != nil { + return arvados.LoginResponse{}, fmt.Errorf("internal error getting redirect target: %s", err) + } + params := url.Values{ + "return_to": []string{options.ReturnTo}, + } + if options.Remote != "" { + params.Set("remote", options.Remote) + } + target.RawQuery = params.Encode() + return arvados.LoginResponse{ + RedirectLocation: target.String(), + }, nil + } else { + return conn.local.Login(ctx, options) + } +} + func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) (arvados.Collection, error) { if len(options.UUID) == 27 { // UUID is really a UUID @@ -210,8 +233,10 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) } else { // UUID is a PDH first := make(chan arvados.Collection, 1) - err := conn.tryLocalThenRemotes(ctx, func(ctx context.Context, remoteID string, be backend) error { - c, err := be.CollectionGet(ctx, options) + err := conn.tryLocalThenRemotes(ctx, options.ForwardedFor, func(ctx context.Context, remoteID string, be backend) error { + remoteOpts := options + remoteOpts.ForwardedFor = conn.cluster.ClusterID + "-" + options.ForwardedFor + c, err := be.CollectionGet(ctx, remoteOpts) if err != nil { return err } @@ -219,8 +244,9 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) // hash+size+hints; only hash+size need to // match the computed PDH. if pdh := portableDataHash(c.ManifestText); pdh != options.UUID && !strings.HasPrefix(options.UUID, pdh+"+") { - ctxlog.FromContext(ctx).Warnf("bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID) - return notFoundError{} + err = httpErrorf(http.StatusBadGateway, "bad portable data hash %q received from remote %q (expected %q)", pdh, remoteID, options.UUID) + ctxlog.FromContext(ctx).Warn(err) + return err } if remoteID != "" { c.ManifestText = rewriteManifest(c.ManifestText, remoteID) @@ -241,15 +267,15 @@ func (conn *Conn) CollectionGet(ctx context.Context, options arvados.GetOptions) } func (conn *Conn) CollectionList(ctx context.Context, options arvados.ListOptions) (arvados.CollectionList, error) { - return conn.local.CollectionList(ctx, options) + return conn.generated_CollectionList(ctx, options) } func (conn *Conn) CollectionProvenance(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) { - return conn.local.CollectionProvenance(ctx, options) + return conn.chooseBackend(options.UUID).CollectionProvenance(ctx, options) } func (conn *Conn) CollectionUsedBy(ctx context.Context, options arvados.GetOptions) (map[string]interface{}, error) { - return conn.local.CollectionUsedBy(ctx, options) + return conn.chooseBackend(options.UUID).CollectionUsedBy(ctx, options) } func (conn *Conn) CollectionDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Collection, error) { @@ -264,6 +290,10 @@ func (conn *Conn) CollectionUntrash(ctx context.Context, options arvados.Untrash 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) } @@ -276,10 +306,6 @@ func (conn *Conn) ContainerGet(ctx context.Context, options arvados.GetOptions) return conn.chooseBackend(options.UUID).ContainerGet(ctx, options) } -func (conn *Conn) ContainerList(ctx context.Context, options arvados.ListOptions) (arvados.ContainerList, error) { - return conn.local.ContainerList(ctx, options) -} - func (conn *Conn) ContainerDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Container, error) { return conn.chooseBackend(options.UUID).ContainerDelete(ctx, options) } @@ -292,6 +318,10 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption 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) } @@ -304,19 +334,153 @@ func (conn *Conn) SpecimenGet(ctx context.Context, options arvados.GetOptions) ( return conn.chooseBackend(options.UUID).SpecimenGet(ctx, options) } -func (conn *Conn) SpecimenList(ctx context.Context, options arvados.ListOptions) (arvados.SpecimenList, error) { - return conn.local.SpecimenList(ctx, options) -} - func (conn *Conn) SpecimenDelete(ctx context.Context, options arvados.DeleteOptions) (arvados.Specimen, error) { 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, + + "etag": false, + "full_name": false, + "identity_url": false, + "is_invited": false, + "owner_uuid": false, + "uuid": false, + "writable_by": 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) } -type backend interface{ Interface } +type backend interface { + arvados.API + BaseURL() url.URL +} type notFoundError struct{}