diff --git a/e2e/aws/eks_test.go b/e2e/aws/eks_test.go index 9a936894c8b78..0d49767341b45 100644 --- a/e2e/aws/eks_test.go +++ b/e2e/aws/eks_test.go @@ -91,6 +91,7 @@ func awsEKSDiscoveryMatchedCluster(t *testing.T) { ) // Get the auth server. authC := teleport.Process.GetAuthServer() + expectedClusterName := os.Getenv(eksClusterNameEnv) // Wait for the discovery service to discover the cluster and create a // KubernetesCluster resource. // Discovery service will scan the AWS account each minutes. @@ -105,7 +106,7 @@ func awsEKSDiscoveryMatchedCluster(t *testing.T) { // Fail fast if the discovery service creates more than one cluster. assert.Len(t, clusters, 1) // Fail fast if the discovery service creates a cluster with a different name. - assert.Equal(t, os.Getenv(eksClusterNameEnv), clusters[0].GetName()) + assert.Equal(t, expectedClusterName, clusters[0].GetName()) return true }, 3*time.Minute, 10*time.Second, "wait for the discovery service to create a cluster") @@ -116,12 +117,19 @@ func awsEKSDiscoveryMatchedCluster(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - kubeServers, err := authC.GetKubernetesServers(ctx) - return err == nil && len(kubeServers) == 1 - }, 2*time.Minute, time.Second, "wait for the kubernetes service to create a KubernetesServer") + kubeServers, err := authC.UnifiedResourceCache.GetKubernetesServers(ctx) + if err != nil { + return false + } - clusters, err := authC.GetKubernetesClusters(context.Background()) - require.NoError(t, err) + for _, ks := range kubeServers { + if ks.GetCluster().GetName() == expectedClusterName { + return true + } + } + + return false + }, 2*time.Minute, time.Second, "wait for the kubernetes service to create a KubernetesServer") // kubeClient is a Kubernetes client for the user created above // that will be used to verify that the user can access the cluster and @@ -131,7 +139,7 @@ func awsEKSDiscoveryMatchedCluster(t *testing.T) { Username: hostUser, KubeUsers: kubeUsers, KubeGroups: kubeGroups, - KubeCluster: clusters[0].GetName(), + KubeCluster: expectedClusterName, }) require.NoError(t, err) diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 8862aa900eb22..f64c28318bbeb 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -99,7 +99,6 @@ import ( "github.com/gravitational/teleport/lib/gitlab" "github.com/gravitational/teleport/lib/inventory" kubetoken "github.com/gravitational/teleport/lib/kube/token" - kubeutils "github.com/gravitational/teleport/lib/kube/utils" "github.com/gravitational/teleport/lib/limiter" "github.com/gravitational/teleport/lib/loginrule" "github.com/gravitational/teleport/lib/modules" @@ -3159,9 +3158,29 @@ func generateCert(ctx context.Context, a *Server, req certRequest, caType types. // If the certificate is targeting a trusted Teleport cluster, it is the // responsibility of the cluster to ensure its existence. if req.routeToCluster == clusterName && req.kubernetesCluster != "" { - if err := kubeutils.CheckKubeCluster(a.closeCtx, a, req.kubernetesCluster); err != nil { + found, _, err := a.UnifiedResourceCache.IterateUnifiedResources(a.closeCtx, func(rwl types.ResourceWithLabels) (bool, error) { + if rwl.GetKind() != types.KindKubeServer { + return false, nil + } + + ks, ok := rwl.(types.KubeServer) + if !ok { + return false, nil + } + + return ks.GetCluster().GetName() == req.kubernetesCluster, nil + }, &proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindKubeServer}, + SortBy: types.SortBy{Field: services.SortByName}, + Limit: 1, + }) + if err != nil { return nil, trace.Wrap(err) } + + if len(found) == 0 { + return nil, trace.BadParameter("Kubernetes cluster %q is not registered in this Teleport cluster; you can list registered Kubernetes clusters using 'tsh kube ls'", req.kubernetesCluster) + } } // See which database names and users this user is allowed to use. diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 4bdd3a693caab..b4510d8419c17 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -140,12 +140,25 @@ func newTestPack( } p.a.SetLockWatcher(lockWatcher) - // set cluster name - err = p.a.SetClusterName(p.clusterName) + urc, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{ + Clock: p.a.clock, + ResourceWatcherConfig: services.ResourceWatcherConfig{ + Component: teleport.ComponentAuth, + Client: p.a, + }, + ResourceGetter: p.a, + }) if err != nil { return p, trace.Wrap(err) } + p.a.SetUnifiedResourcesCache(urc) + + // set cluster name + if err := p.a.SetClusterName(p.clusterName); err != nil { + return p, trace.Wrap(err) + } + // set static tokens staticTokens, err := types.NewStaticTokens(types.StaticTokensSpecV2{ StaticTokens: []types.ProvisionTokenV1{}, @@ -655,6 +668,22 @@ func TestAuthenticateSSHUser(t *testing.T) { _, err = s.a.UpsertKubernetesServer(ctx, kubeServer) require.NoError(t, err) + // Wait for cache propagation of the kubernetes resources before proceeding with the tests. + require.Eventually(t, func() bool { + kubeServers, err := s.a.UnifiedResourceCache.GetKubernetesServers(ctx) + if err != nil { + return false + } + + for _, ks := range kubeServers { + if ks.GetCluster().GetName() == kubeCluster.GetName() { + return true + } + } + + return false + }, 10*time.Second, 100*time.Millisecond) + // Login specifying a valid kube cluster. It should appear in the TLS cert. resp, err = s.a.AuthenticateSSHUser(ctx, authclient.AuthenticateSSHRequest{ AuthenticateUserRequest: authclient.AuthenticateUserRequest{ @@ -2993,6 +3022,98 @@ func TestGenerateUserCertWithHardwareKeySupport(t *testing.T) { } } +func TestGenerateKubernetesUserCert(t *testing.T) { + ctx := context.Background() + p, err := newTestPack(ctx, t.TempDir()) + require.NoError(t, err) + + user, _, err := CreateUserAndRole(p.a, "test-user", []string{}, nil) + require.NoError(t, err) + + rc, err := types.NewRemoteCluster("leaf") + require.NoError(t, err) + _, err = p.a.CreateRemoteCluster(ctx, rc) + require.NoError(t, err) + + kubeCluster, err := types.NewKubernetesClusterV3(types.Metadata{Name: "kube-cluster"}, types.KubernetesClusterSpecV3{}) + require.NoError(t, err) + kubeServer, err := types.NewKubernetesServerV3FromCluster(kubeCluster, "foo", "1") + require.NoError(t, err) + _, err = p.a.UpsertKubernetesServer(ctx, kubeServer) + require.NoError(t, err) + + // Wait for cache propagation of the kubernetes resources before proceeding with the tests. + require.EventuallyWithT(t, func(t *assert.CollectT) { + found, _, err := p.a.UnifiedResourceCache.IterateUnifiedResources(ctx, func(rwl types.ResourceWithLabels) (bool, error) { + if rwl.GetKind() != types.KindKubeServer { + return false, nil + } + + ks, ok := rwl.(types.KubeServer) + if !ok { + return false, nil + } + + return ks.GetCluster().GetName() == kubeCluster.GetName(), nil + }, &proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindKubeServer}, + SortBy: types.SortBy{Field: services.SortByName}, + Limit: 1, + }) + + assert.NoError(t, err) + assert.Len(t, found, 1) + }, 10*time.Second, 100*time.Millisecond) + + accessInfo := services.AccessInfoFromUserState(user) + accessChecker, err := services.NewAccessChecker(accessInfo, p.clusterName.GetClusterName(), p.a) + require.NoError(t, err) + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + key, err := keys.NewPrivateKey(priv, nil) + require.NoError(t, err) + + for _, tt := range []struct { + name string + teleportCluster string + kubernetesCluster string + assertErr require.ErrorAssertionFunc + }{ + { + name: "leaf clusters not validated", + teleportCluster: "leaf", + kubernetesCluster: "foo", + assertErr: require.NoError, + }, + { + name: "kubernetes cluster not registered", + teleportCluster: p.clusterName.GetClusterName(), + kubernetesCluster: "foo", + assertErr: require.Error, + }, + { + name: "kubernetes cluster registered", + teleportCluster: p.clusterName.GetClusterName(), + kubernetesCluster: kubeCluster.GetName(), + assertErr: require.NoError, + }, + } { + t.Run(tt.name, func(t *testing.T) { + certReq := certRequest{ + user: user, + checker: accessChecker, + publicKey: key.MarshalSSHPublicKey(), + routeToCluster: tt.teleportCluster, + kubernetesCluster: tt.kubernetesCluster, + } + + _, err = p.a.generateUserCert(ctx, certReq) + tt.assertErr(t, err) + }) + } +} + func TestNewWebSession(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/lib/kube/utils/utils.go b/lib/kube/utils/utils.go index 91a6a5f11fa66..0806453e356f1 100644 --- a/lib/kube/utils/utils.go +++ b/lib/kube/utils/utils.go @@ -22,7 +22,6 @@ import ( "context" "encoding/hex" "errors" - "slices" "strings" "github.com/gravitational/trace" @@ -147,48 +146,6 @@ func EncodeClusterName(clusterName string) string { return "k" + hex.EncodeToString([]byte(clusterName)) } -// KubeServicesPresence fetches a list of registered kubernetes servers. -// It's a subset of services.Presence. -type KubeServicesPresence interface { - // GetKubernetesServers returns a list of registered kubernetes servers. - GetKubernetesServers(context.Context) ([]types.KubeServer, error) -} - -// KubeClusterNames returns a sorted list of unique kubernetes cluster -// names registered in p. -// -// DELETE IN 11.0.0, replaced by ListKubeClustersWithFilters -func KubeClusterNames(ctx context.Context, p KubeServicesPresence) ([]string, error) { - kss, err := p.GetKubernetesServers(ctx) - if err != nil { - return nil, trace.Wrap(err) - } - return extractAndSortKubeClusterNames(kss), nil -} - -func extractAndSortKubeClusterNames(kubeServers []types.KubeServer) []string { - kubeClusters := extractAndSortKubeClusters(kubeServers) - kubeClusterNames := make([]string, len(kubeClusters)) - for i := range kubeClusters { - kubeClusterNames[i] = kubeClusters[i].GetName() - } - - return kubeClusterNames -} - -// KubeClusters returns a sorted list of unique kubernetes clusters -// registered in p. -// -// DELETE IN 11.0.0, replaced by ListKubeClustersWithFilters -func KubeClusters(ctx context.Context, p KubeServicesPresence) ([]types.KubeCluster, error) { - kubeServers, err := p.GetKubernetesServers(ctx) - if err != nil { - return nil, trace.Wrap(err) - } - - return extractAndSortKubeClusters(kubeServers), nil -} - // ListKubeClustersWithFilters returns a sorted list of unique kubernetes clusters // registered in p. func ListKubeClustersWithFilters(ctx context.Context, p client.GetResourcesClient, req proto.ListResourcesRequest) ([]types.KubeCluster, error) { @@ -244,19 +201,3 @@ func GetKubeAgentVersion(ctx context.Context, pinger Pinger, clusterFeatures pro return strings.TrimPrefix(agentVersion, "v"), nil } - -// CheckKubeCluster validates kubeClusterName is registered with this Teleport cluster. -func CheckKubeCluster(ctx context.Context, p KubeServicesPresence, kubeClusterName string) error { - if kubeClusterName == "" { - return trace.BadParameter("kube cluster name should not be empty.") - } - kubeClusterNames, err := KubeClusterNames(ctx, p) - if err != nil { - return trace.Wrap(err, "failed to get list of available Kubernetes clusters.") - } - if !slices.Contains(kubeClusterNames, kubeClusterName) { - return trace.BadParameter("Kubernetes cluster %q is not registered in this Teleport cluster; you can list registered Kubernetes clusters using 'tsh kube ls'", kubeClusterName) - } - - return nil -} diff --git a/lib/kube/utils/utils_test.go b/lib/kube/utils/utils_test.go index 030399a6abbea..b7df332c51c07 100644 --- a/lib/kube/utils/utils_test.go +++ b/lib/kube/utils/utils_test.go @@ -26,74 +26,9 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/automaticupgrades" ) -func TestCheckKubeCluster(t *testing.T) { - t.Parallel() - ctx := context.Background() - - kubeServers := []types.KubeServer{ - kubeServer(t, "k8s-1", "server1", "uuuid"), - kubeServer(t, "k8s-2", "server1", "uuuid"), - kubeServer(t, "k8s-3", "server1", "uuuid"), - kubeServer(t, "k8s-4", "server1", "uuuid"), - } - - tests := []struct { - desc string - services []types.KubeServer - kubeCluster string - assertErr require.ErrorAssertionFunc - }{ - { - desc: "valid cluster name", - services: kubeServers, - kubeCluster: "k8s-4", - assertErr: require.NoError, - }, - { - desc: "invalid cluster name", - services: kubeServers, - kubeCluster: "k8s-5", - assertErr: require.Error, - }, - { - desc: "no registered clusters", - services: []types.KubeServer{}, - kubeCluster: "k8s-1", - assertErr: require.Error, - }, - { - desc: "empty cluster provided", - services: kubeServers, - kubeCluster: "", - assertErr: require.Error, - }, - } - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - err := CheckKubeCluster(ctx, mockKubeServicesPresence(tt.services), tt.kubeCluster) - tt.assertErr(t, err) - }) - } -} - -type mockKubeServicesPresence []types.KubeServer - -func (p mockKubeServicesPresence) GetKubernetesServers(context.Context) ([]types.KubeServer, error) { - return p, nil -} - -func kubeServer(t *testing.T, kubeCluster, hostname, hostID string) types.KubeServer { - cluster, err := types.NewKubernetesClusterV3(types.Metadata{Name: kubeCluster}, types.KubernetesClusterSpecV3{}) - require.NoError(t, err) - server, err := types.NewKubernetesServerV3FromCluster(cluster, hostname, hostID) - require.NoError(t, err) - return server -} - func TestGetAgentVersion(t *testing.T) { t.Parallel() @@ -161,20 +96,3 @@ type pinger struct { func (p *pinger) Ping(ctx context.Context) (proto.PingResponse, error) { return p.pingFn(ctx) } - -func TestExtractAndSortKubeClusterNames(t *testing.T) { - t.Parallel() - - server1 := kubeServer(t, "watermelon", "server1", "uuuid") - - server2 := kubeServer(t, "watermelon", "server1", "uuuid") - - server3 := kubeServer(t, "banana", "server2", "uuuid2") - - server4 := kubeServer(t, "apple", "server2", "uuuid2") - - server5 := kubeServer(t, "pear", "server2", "uuuid2") - - names := extractAndSortKubeClusterNames(types.KubeServers{server1, server2, server3, server4, server5}) - require.Equal(t, []string{"apple", "banana", "pear", "watermelon"}, names) -} diff --git a/tool/tctl/common/auth_command.go b/tool/tctl/common/auth_command.go index 0e96a04ad4243..c1b09e91c2754 100644 --- a/tool/tctl/common/auth_command.go +++ b/tool/tctl/common/auth_command.go @@ -47,7 +47,6 @@ import ( "github.com/gravitational/teleport/lib/client/db" "github.com/gravitational/teleport/lib/client/identityfile" "github.com/gravitational/teleport/lib/defaults" - kubeutils "github.com/gravitational/teleport/lib/kube/utils" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" @@ -308,7 +307,6 @@ func (a *AuthCommand) GenerateKeys(ctx context.Context) error { // certificateSigner is an interface for the methods used by GenerateAndSignKeys // to sign certificates using the Auth Server. type certificateSigner interface { - kubeutils.KubeServicesPresence GenerateDatabaseCert(context.Context, *proto.DatabaseCertRequest) (*proto.DatabaseCertResponse, error) GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) GenerateWindowsDesktopCert(context.Context, *proto.WindowsDesktopCertRequest) (*proto.WindowsDesktopCertResponse, error) @@ -913,7 +911,7 @@ func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI certifica } key.ClusterName = a.leafCluster - if err := a.checkKubeCluster(ctx, clusterAPI); err != nil { + if err := a.checkKubeCluster(); err != nil { return trace.Wrap(err) } @@ -1073,7 +1071,7 @@ func (a *AuthCommand) checkLeafCluster(clusterAPI certificateSigner) error { return trace.BadParameter("couldn't find leaf cluster named %q", a.leafCluster) } -func (a *AuthCommand) checkKubeCluster(ctx context.Context, clusterAPI certificateSigner) error { +func (a *AuthCommand) checkKubeCluster() error { if a.kubeCluster == "" { return nil } @@ -1086,20 +1084,6 @@ func (a *AuthCommand) checkKubeCluster(ctx context.Context, clusterAPI certifica return nil } - localCluster, err := clusterAPI.GetClusterName() - if err != nil { - return trace.Wrap(err) - } - if localCluster.GetClusterName() != a.leafCluster { - // Skip validation on remote clusters, since we don't know their - // registered kube clusters. - return nil - } - - if err := kubeutils.CheckKubeCluster(ctx, clusterAPI, a.kubeCluster); err != nil { - return trace.Wrap(err) - } - return nil } diff --git a/tool/tctl/common/auth_command_test.go b/tool/tctl/common/auth_command_test.go index 813c9e81e16ff..2f7fc36af0847 100644 --- a/tool/tctl/common/auth_command_test.go +++ b/tool/tctl/common/auth_command_test.go @@ -470,110 +470,6 @@ func (c *mockClient) GenerateCertAuthorityCRL(context.Context, types.CertAuthTyp return c.crl, nil } -func TestCheckKubeCluster(t *testing.T) { - const teleportCluster = "local-teleport" - clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ - ClusterName: teleportCluster, - }) - require.NoError(t, err) - client := &mockClient{ - clusterName: clusterName, - } - tests := []struct { - desc string - kubeCluster string - leafCluster string - outputFormat identityfile.Format - registeredClusters []*types.KubernetesClusterV3 - want string - assertErr require.ErrorAssertionFunc - }{ - { - desc: "non-k8s output format", - outputFormat: identityfile.FormatFile, - assertErr: require.NoError, - }, - { - desc: "local cluster, valid kube cluster", - kubeCluster: "foo", - leafCluster: teleportCluster, - registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}}, - outputFormat: identityfile.FormatKubernetes, - want: "foo", - assertErr: require.NoError, - }, - { - desc: "local cluster, empty kube cluster", - kubeCluster: "", - leafCluster: teleportCluster, - registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}}, - outputFormat: identityfile.FormatKubernetes, - assertErr: require.NoError, - }, - { - desc: "local cluster, empty kube cluster, no registered kube clusters", - kubeCluster: "", - leafCluster: teleportCluster, - registeredClusters: []*types.KubernetesClusterV3{}, - outputFormat: identityfile.FormatKubernetes, - want: "", - assertErr: require.NoError, - }, - { - desc: "local cluster, invalid kube cluster", - kubeCluster: "bar", - leafCluster: teleportCluster, - registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}}, - outputFormat: identityfile.FormatKubernetes, - assertErr: require.Error, - }, - { - desc: "remote cluster, empty kube cluster", - kubeCluster: "", - leafCluster: "remote-teleport", - registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}}, - outputFormat: identityfile.FormatKubernetes, - want: "", - assertErr: require.NoError, - }, - { - desc: "remote cluster, non-empty kube cluster", - kubeCluster: "bar", - leafCluster: "remote-teleport", - registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}}, - outputFormat: identityfile.FormatKubernetes, - want: "bar", - assertErr: require.NoError, - }, - } - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - client.kubeServers = []types.KubeServer{} - for _, kube := range tt.registeredClusters { - client.kubeServers = append(client.kubeServers, &types.KubernetesServerV3{ - Metadata: types.Metadata{ - Name: kube.GetName(), - }, - Spec: types.KubernetesServerSpecV3{ - Hostname: "host", - Cluster: kube, - }, - }) - } - a := &AuthCommand{ - kubeCluster: tt.kubeCluster, - leafCluster: tt.leafCluster, - outputFormat: tt.outputFormat, - } - err := a.checkKubeCluster(context.Background(), client) - tt.assertErr(t, err) - if err == nil { - require.Equal(t, tt.want, a.kubeCluster) - } - }) - } -} - // TestGenerateDatabaseKeys verifies cert/key pair generation for databases. func TestGenerateDatabaseKeys(t *testing.T) { clusterName, err := services.NewClusterNameWithRandomID( diff --git a/tool/tsh/common/kube_test.go b/tool/tsh/common/kube_test.go index 5cd3aefd1ffbd..b27868b2bb426 100644 --- a/tool/tsh/common/kube_test.go +++ b/tool/tsh/common/kube_test.go @@ -167,9 +167,9 @@ func setupKubeTestPack(t *testing.T, withMultiplexMode bool) *kubeTestPack { }, ), withValidationFunc(func(s *suite) bool { - rootClusters, err := s.root.GetAuthServer().GetKubernetesServers(ctx) + rootClusters, err := s.root.GetAuthServer().UnifiedResourceCache.GetKubernetesServers(ctx) require.NoError(t, err) - leafClusters, err := s.leaf.GetAuthServer().GetKubernetesServers(ctx) + leafClusters, err := s.leaf.GetAuthServer().UnifiedResourceCache.GetKubernetesServers(ctx) require.NoError(t, err) return len(rootClusters) >= 2 && len(leafClusters) >= 1 }), diff --git a/tool/tsh/common/tsh_helper_test.go b/tool/tsh/common/tsh_helper_test.go index 0a620358c7b2b..39de0a77f179f 100644 --- a/tool/tsh/common/tsh_helper_test.go +++ b/tool/tsh/common/tsh_helper_test.go @@ -476,7 +476,7 @@ func mustRegisterKubeClusters(t *testing.T, ctx context.Context, authSrv *auth.S require.NoError(t, wg.Wait()) require.EventuallyWithT(t, func(c *assert.CollectT) { - servers, err := authSrv.GetKubernetesServers(ctx) + servers, err := authSrv.UnifiedResourceCache.GetKubernetesServers(ctx) assert.NoError(c, err) gotNames := map[string]struct{}{} for _, ks := range servers { diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index a28d35031a835..8dd0253ecae7d 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -2600,6 +2600,29 @@ func TestKubeCredentialsLock(t *testing.T) { _, err = authServer.UpsertKubernetesServer(context.Background(), kubeServer) require.NoError(t, err) + require.EventuallyWithT(t, func(t *assert.CollectT) { + found, _, err := authServer.UnifiedResourceCache.IterateUnifiedResources(ctx, func(rwl types.ResourceWithLabels) (bool, error) { + if rwl.GetKind() != types.KindKubeServer { + return false, nil + } + + ks, ok := rwl.(types.KubeServer) + if !ok { + return false, nil + } + + return ks.GetCluster().GetName() == kubeCluster.GetName(), nil + }, &proto.ListUnifiedResourcesRequest{ + Kinds: []string{types.KindKubeServer}, + SortBy: types.SortBy{Field: services.SortByName}, + Limit: 1, + }) + + assert.NoError(t, err) + assert.Len(t, found, 1) + + }, 10*time.Second, 100*time.Millisecond) + var ssoCalls atomic.Int32 mockSSOLogin := mockSSOLogin(authServer, alice) mockSSOLoginWithCountCalls := func(ctx context.Context, connectorID string, priv *keys.PrivateKey, protocol string) (*authclient.SSHLoginResponse, error) {