diff --git a/lib/teleterm/clusters/cluster.go b/lib/teleterm/clusters/cluster.go index b2e53606a9375..0a0f20f24ff9c 100644 --- a/lib/teleterm/clusters/cluster.go +++ b/lib/teleterm/clusters/cluster.go @@ -24,10 +24,12 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/types" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/auth" @@ -85,73 +87,97 @@ func (c *Cluster) Connected() bool { // and enabled enterprise features. This method requires a valid cert. func (c *Cluster) GetWithDetails(ctx context.Context, authClient auth.ClientI) (*ClusterWithDetails, error) { var ( - authPingResponse proto.PingResponse - caps *types.AccessCapabilities - authClusterID string - acl *api.ACL - user types.User + clusterPingResponse *webclient.PingResponse + authPingResponse proto.PingResponse + caps *types.AccessCapabilities + authClusterID string + acl *api.ACL + user types.User + roles []types.Role ) - clusterPingResponse, err := c.clusterClient.Ping(ctx) - if err != nil { - return nil, trace.Wrap(err) - } + group, groupCtx := errgroup.WithContext(ctx) - //TODO(gzdunek): These calls should be done in parallel. - err = AddMetadataToRetryableError(ctx, func() error { - authPingResponse, err = authClient.Ping(ctx) - if err != nil { - return trace.Wrap(err) - } + group.Go(func() error { + res, err := c.clusterClient.Ping(groupCtx) + clusterPingResponse = res + return trace.Wrap(err) + }) - caps, err = authClient.GetAccessCapabilities(ctx, types.AccessCapabilitiesRequest{ - RequestableRoles: true, - SuggestedReviewers: true, - }) - if err != nil { + group.Go(func() error { + err := AddMetadataToRetryableError(groupCtx, func() error { + res, err := authClient.Ping(groupCtx) + authPingResponse = res return trace.Wrap(err) - } + }) + return trace.Wrap(err) + }) - clusterName, err := authClient.GetClusterName() - if err != nil { + group.Go(func() error { + err := AddMetadataToRetryableError(groupCtx, func() error { + res, err := authClient.GetAccessCapabilities(groupCtx, types.AccessCapabilitiesRequest{ + RequestableRoles: true, + SuggestedReviewers: true, + }) + caps = res return trace.Wrap(err) - } - authClusterID = clusterName.GetClusterID() + }) + return trace.Wrap(err) + }) - user, err = authClient.GetCurrentUser(ctx) - if err != nil { - return trace.Wrap(err) - } + group.Go(func() error { + err := AddMetadataToRetryableError(groupCtx, func() error { + clusterName, err := authClient.GetClusterName() + if err != nil { + return trace.Wrap(err) + } + authClusterID = clusterName.GetClusterID() + return nil + }) + return trace.Wrap(err) + }) - roles, err := authClient.GetCurrentUserRoles(ctx) - if err != nil { + group.Go(func() error { + err := AddMetadataToRetryableError(groupCtx, func() error { + res, err := authClient.GetCurrentUser(groupCtx) + user = res return trace.Wrap(err) - } + }) + return trace.Wrap(err) + }) - roleSet := services.NewRoleSet(roles...) - userACL := services.NewUserACL(user, roleSet, *authPingResponse.ServerFeatures, false, false) - - acl = &api.ACL{ - RecordedSessions: convertToAPIResourceAccess(userACL.RecordedSessions), - ActiveSessions: convertToAPIResourceAccess(userACL.ActiveSessions), - AuthConnectors: convertToAPIResourceAccess(userACL.AuthConnectors), - Roles: convertToAPIResourceAccess(userACL.Roles), - Users: convertToAPIResourceAccess(userACL.Users), - TrustedClusters: convertToAPIResourceAccess(userACL.TrustedClusters), - Events: convertToAPIResourceAccess(userACL.Events), - Tokens: convertToAPIResourceAccess(userACL.Tokens), - Servers: convertToAPIResourceAccess(userACL.Nodes), - Apps: convertToAPIResourceAccess(userACL.AppServers), - Dbs: convertToAPIResourceAccess(userACL.DBServers), - Kubeservers: convertToAPIResourceAccess(userACL.KubeServers), - AccessRequests: convertToAPIResourceAccess(userACL.AccessRequests), - } - return nil + group.Go(func() error { + err := AddMetadataToRetryableError(groupCtx, func() error { + res, err := authClient.GetCurrentUserRoles(groupCtx) + roles = res + return trace.Wrap(err) + }) + return trace.Wrap(err) }) - if err != nil { + + if err := group.Wait(); err != nil { return nil, trace.Wrap(err) } + roleSet := services.NewRoleSet(roles...) + userACL := services.NewUserACL(user, roleSet, *authPingResponse.ServerFeatures, false, false) + + acl = &api.ACL{ + RecordedSessions: convertToAPIResourceAccess(userACL.RecordedSessions), + ActiveSessions: convertToAPIResourceAccess(userACL.ActiveSessions), + AuthConnectors: convertToAPIResourceAccess(userACL.AuthConnectors), + Roles: convertToAPIResourceAccess(userACL.Roles), + Users: convertToAPIResourceAccess(userACL.Users), + TrustedClusters: convertToAPIResourceAccess(userACL.TrustedClusters), + Events: convertToAPIResourceAccess(userACL.Events), + Tokens: convertToAPIResourceAccess(userACL.Tokens), + Servers: convertToAPIResourceAccess(userACL.Nodes), + Apps: convertToAPIResourceAccess(userACL.AppServers), + Dbs: convertToAPIResourceAccess(userACL.DBServers), + Kubeservers: convertToAPIResourceAccess(userACL.KubeServers), + AccessRequests: convertToAPIResourceAccess(userACL.AccessRequests), + } + withDetails := &ClusterWithDetails{ Cluster: c, SuggestedReviewers: caps.SuggestedReviewers, diff --git a/lib/teleterm/services/userpreferences/userpreferences.go b/lib/teleterm/services/userpreferences/userpreferences.go index 39676d052ecd6..a8e1b9d6ae273 100644 --- a/lib/teleterm/services/userpreferences/userpreferences.go +++ b/lib/teleterm/services/userpreferences/userpreferences.go @@ -20,25 +20,39 @@ import ( "context" "github.com/gravitational/trace" + "golang.org/x/sync/errgroup" userpreferencesv1 "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" ) func Get(ctx context.Context, rootClient Client, leafClient Client) (*api.UserPreferences, error) { - rootPreferencesResponse, err := rootClient.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{}) - if err != nil { + group, groupCtx := errgroup.WithContext(ctx) + var rootPreferencesResponse *userpreferencesv1.GetUserPreferencesResponse + var leafPreferencesResponse *userpreferencesv1.GetUserPreferencesResponse + + group.Go(func() error { + res, err := rootClient.GetUserPreferences(groupCtx, &userpreferencesv1.GetUserPreferencesRequest{}) + rootPreferencesResponse = res + return trace.Wrap(err) + }) + + if leafClient != nil { + group.Go(func() error { + res, err := leafClient.GetUserPreferences(groupCtx, &userpreferencesv1.GetUserPreferencesRequest{}) + leafPreferencesResponse = res + return trace.Wrap(err) + }) + } + + if err := group.Wait(); err != nil { return nil, trace.Wrap(err) } + rootPreferences := rootPreferencesResponse.GetPreferences() clusterPreferences := rootPreferences.GetClusterPreferences() - - if leafClient != nil { - preferences, err := leafClient.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{}) - if err != nil { - return nil, trace.Wrap(err) - } - clusterPreferences = preferences.GetPreferences().GetClusterPreferences() + if leafPreferencesResponse != nil { + clusterPreferences = leafPreferencesResponse.GetPreferences().GetClusterPreferences() } return &api.UserPreferences{ @@ -48,27 +62,55 @@ func Get(ctx context.Context, rootClient Client, leafClient Client) (*api.UserPr } // Update updates the preferences for a given user. -// Only the properties that are set (cluster_preferences, unified_resource_preferences) will be updated. +// Only the properties that are set (cluster_preferences, unified_resource_preferences) are updated. +// When updating the preferences for the root cluster, both unified_resource_preferences +// and cluster_preferences are updated in it. +// When updating the preferences for the leaf cluster, only cluster_preferences are updated +// in the leaf, unified_resource_preferences are always updated in the root. func Update(ctx context.Context, rootClient Client, leafClient Client, newPreferences *api.UserPreferences) (*api.UserPreferences, error) { // We have to fetch the full user preferences struct and modify only // the fields that change. // Calling `UpsertUserPreferences` with only the modified values would reset // the rest of the preferences. - rootPreferencesResponse, err := rootClient.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{}) - if err != nil { + getGroup, getGroupCtx := errgroup.WithContext(ctx) + var rootPreferencesResponse *userpreferencesv1.GetUserPreferencesResponse + var leafPreferencesResponse *userpreferencesv1.GetUserPreferencesResponse + + getGroup.Go(func() error { + res, err := rootClient.GetUserPreferences(getGroupCtx, &userpreferencesv1.GetUserPreferencesRequest{}) + rootPreferencesResponse = res + return trace.Wrap(err) + }) + + if leafClient != nil { + getGroup.Go(func() error { + res, err := leafClient.GetUserPreferences(getGroupCtx, &userpreferencesv1.GetUserPreferencesRequest{}) + leafPreferencesResponse = res + return trace.Wrap(err) + }) + } + + if err := getGroup.Wait(); err != nil { return nil, trace.Wrap(err) } - rootPreferences := rootPreferencesResponse.GetPreferences() + rootPreferences := rootPreferencesResponse.GetPreferences() var leafPreferences *userpreferencesv1.UserPreferences - if leafClient != nil { - response, err := leafClient.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{}) - if err != nil { - return nil, trace.Wrap(err) - } - leafPreferences = response.GetPreferences() + if leafPreferencesResponse != nil { + leafPreferences = leafPreferencesResponse.GetPreferences() } + // We do not use errgroup.WithContext since we don't want to cancel + // the other request when one of them fails. + // + // We can run update requests concurrently because the preferences for the root + // cluster and the leaf cluster aren't dependent on each other. + // The preferences for the unified view are always set for the root cluster, + // while pinned resources can be set for either the root or the leaf. + // So if, for example, setting unified view preferences fails, + // we can still update pinned resources for leaf. + upsertGroup := errgroup.Group{} + hasUnifiedResourcePreferencesForRoot := newPreferences.UnifiedResourcePreferences != nil hasClusterPreferencesForRoot := newPreferences.ClusterPreferences != nil && leafPreferences == nil @@ -80,24 +122,28 @@ func Update(ctx context.Context, rootClient Client, leafClient Client, newPrefer rootPreferences.ClusterPreferences = updateClusterPreferences(rootPreferences.ClusterPreferences, newPreferences.ClusterPreferences) } - err := rootClient.UpsertUserPreferences(ctx, &userpreferencesv1.UpsertUserPreferencesRequest{ - Preferences: rootPreferences, + upsertGroup.Go(func() error { + err := rootClient.UpsertUserPreferences(ctx, &userpreferencesv1.UpsertUserPreferencesRequest{ + Preferences: rootPreferences, + }) + return trace.Wrap(err) }) - if err != nil { - return nil, trace.Wrap(err) - } } hasClusterPreferencesForLeaf := newPreferences.ClusterPreferences != nil && leafPreferences != nil if hasClusterPreferencesForLeaf { leafPreferences.ClusterPreferences = updateClusterPreferences(leafPreferences.ClusterPreferences, newPreferences.ClusterPreferences) - err := leafClient.UpsertUserPreferences(ctx, &userpreferencesv1.UpsertUserPreferencesRequest{ - Preferences: leafPreferences, + upsertGroup.Go(func() error { + err := leafClient.UpsertUserPreferences(ctx, &userpreferencesv1.UpsertUserPreferencesRequest{ + Preferences: leafPreferences, + }) + return trace.Wrap(err) }) - if err != nil { - return nil, trace.Wrap(err) - } + } + + if err := upsertGroup.Wait(); err != nil { + return nil, trace.Wrap(err) } updatedPreferences := &api.UserPreferences{