diff --git a/go.mod b/go.mod index d12d5d3c2ff2b..00f3b1bee1926 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ec2 v1.142.1 github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect v1.20.6 github.com/aws/aws-sdk-go-v2/service/ecs v1.36.0 + github.com/aws/aws-sdk-go-v2/service/eks v1.37.1 github.com/aws/aws-sdk-go-v2/service/glue v1.73.1 github.com/aws/aws-sdk-go-v2/service/iam v1.28.7 github.com/aws/aws-sdk-go-v2/service/rds v1.66.2 diff --git a/go.sum b/go.sum index aafc3081ba8eb..a383342c517b9 100644 --- a/go.sum +++ b/go.sum @@ -240,6 +240,8 @@ github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect v1.20.6 h1:Y0pqdpafA8TdG github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect v1.20.6/go.mod h1:y6fUhf01cjz+VUz+zrmJh3KfIXhefV7dS4STCxgHx7g= github.com/aws/aws-sdk-go-v2/service/ecs v1.36.0 h1:XjN5jaDmvP0fDGEOn/Ws06wNKNXUAPGLdeBhKUetQcc= github.com/aws/aws-sdk-go-v2/service/ecs v1.36.0/go.mod h1:kt+L4lMA2nvv9evq9S6TOH1up95/2RsQG4GXfxoPRfM= +github.com/aws/aws-sdk-go-v2/service/eks v1.37.1 h1:5eFw5vlZI2KOChY0DOWxsnuC6N01WC3ZUo5+lco9mN8= +github.com/aws/aws-sdk-go-v2/service/eks v1.37.1/go.mod h1:0R62cZb66e+iaJU7jG3GQbenxD8B7kh4UFNZ19pauTA= github.com/aws/aws-sdk-go-v2/service/glue v1.73.1 h1:z/NBYW8RygzWrDgNWib10fuLUBl0SLj0KruGoEHxnKQ= github.com/aws/aws-sdk-go-v2/service/glue v1.73.1/go.mod h1:F3B9DC5FsIHAxUtHZdY5KUeqN+tHoGlRPzSSYdXjC38= github.com/aws/aws-sdk-go-v2/service/iam v1.28.7 h1:FKPRDYZOO0Eur19vWUL1B40Op0j89KQj3kARjrszMK8= diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index 94393d3601958..6079c5d352f81 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -29,8 +29,6 @@ import ( "testing" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/eks" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" @@ -2041,11 +2039,11 @@ func TestKubernetesClusterCRUD_DiscoveryService(t *testing.T) { discoveryClt, err := srv.NewClient(TestBuiltin(types.RoleDiscovery)) require.NoError(t, err) - eksCluster, err := services.NewKubeClusterFromAWSEKS(&eks.Cluster{ - Name: aws.String("eks-cluster1"), - Arn: aws.String("arn:aws:eks:eu-west-1:accountID:cluster/cluster1"), - Status: aws.String(eks.ClusterStatusActive), - }) + eksCluster, err := services.NewKubeClusterFromAWSEKS( + "eks-cluster1", + "arn:aws:eks:eu-west-1:accountID:cluster/cluster1", + nil, + ) require.NoError(t, err) eksCluster.SetOrigin(types.OriginCloud) @@ -2061,11 +2059,11 @@ func TestKubernetesClusterCRUD_DiscoveryService(t *testing.T) { require.NoError(t, srv.Auth().CreateKubernetesCluster(ctx, nonCloudCluster)) // Discovery service cannot create cluster with dynamic labels. - clusterWithDynamicLabels, err := services.NewKubeClusterFromAWSEKS(&eks.Cluster{ - Name: aws.String("eks-cluster2"), - Arn: aws.String("arn:aws:eks:eu-west-1:accountID:cluster/cluster2"), - Status: aws.String(eks.ClusterStatusActive), - }) + clusterWithDynamicLabels, err := services.NewKubeClusterFromAWSEKS( + "eks-cluster2", + "arn:aws:eks:eu-west-1:accountID:cluster/cluster2", + nil, + ) require.NoError(t, err) clusterWithDynamicLabels.SetOrigin(types.OriginCloud) clusterWithDynamicLabels.SetDynamicLabels(map[string]types.CommandLabel{ diff --git a/lib/integrations/awsoidc/clients.go b/lib/integrations/awsoidc/clients.go index 1f3b46f07a4fa..3969920523e29 100644 --- a/lib/integrations/awsoidc/clients.go +++ b/lib/integrations/awsoidc/clients.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2instanceconnect" "github.com/aws/aws-sdk-go-v2/service/ecs" + "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gravitational/trace" @@ -95,6 +96,15 @@ func newAWSConfig(ctx context.Context, req *AWSClientRequest) (*aws.Config, erro return &cfg, nil } +func newEKSClient(ctx context.Context, req *AWSClientRequest) (*eks.Client, error) { + cfg, err := newAWSConfig(ctx, req) + if err != nil { + return nil, trace.Wrap(err) + } + + return eks.NewFromConfig(*cfg), nil +} + // newRDSClient creates an [rds.Client] using the provided Token, RoleARN and Region. func newRDSClient(ctx context.Context, req *AWSClientRequest) (*rds.Client, error) { cfg, err := newAWSConfig(ctx, req) @@ -145,7 +155,7 @@ func newEC2InstanceConnectClient(ctx context.Context, req *AWSClientRequest) (*e return ec2instanceconnect.NewFromConfig(*cfg), nil } -// newAWSCredentialsProvider creates an [aws.CredentialsRetriever] using the provided Token, RoleARN and Region. +// newAWSCredentialsProvider creates an [aws.CredentialsProvider] using the provided Token, RoleARN and Region. func newAWSCredentialsProvider(ctx context.Context, req *AWSClientRequest) (aws.CredentialsProvider, error) { cfg, err := newAWSConfig(ctx, req) if err != nil { diff --git a/lib/integrations/awsoidc/eks_list_clusters.go b/lib/integrations/awsoidc/eks_list_clusters.go new file mode 100644 index 0000000000000..a8877962a0e3b --- /dev/null +++ b/lib/integrations/awsoidc/eks_list_clusters.go @@ -0,0 +1,185 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package awsoidc + +import ( + "context" + "strings" + "sync" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/aws/aws-sdk-go-v2/service/eks" + eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/gravitational/trace" + "golang.org/x/sync/errgroup" + + "github.com/gravitational/teleport/api/types" +) + +// ListEKSClustersRequest contains the required fields to list AWS EKS Clusters. +type ListEKSClustersRequest struct { + // Region is the AWS Region. + Region string + + // NextToken is the token to be used to fetch the next page. + // If empty, the first page is fetched. + NextToken string +} + +// CheckAndSetDefaults checks if the required fields are present. +func (req *ListEKSClustersRequest) CheckAndSetDefaults() error { + if req.Region == "" { + return trace.BadParameter("region is required") + } + + return nil +} + +// EKSCluster represents a cluster in AWS EKS. +type EKSCluster struct { + // Name is the name of AWS EKS cluster. + Name string + + // Region is an AWS region. + Region string + + // Labels are labels of a EKS cluster. + Labels map[string]string + + // JoinLabels are Teleport labels that should be injected into kube agent + // if the cluster will be enrolled into Teleport (agent installed on it). + JoinLabels map[string]string + + // Status is a current status of an EKS cluster in AWS. + Status string +} + +// ListEKSClustersResponse contains a page of AWS EKS Clusters. +type ListEKSClustersResponse struct { + // Servers contains the page of Servers. + Clusters []EKSCluster + + // NextToken is used for pagination. + // If non-empty, it can be used to request the next page. + NextToken string + + // ClusterFetchingErrors contains errors for fetching detailed information about specific cluster, if any happened. + ClusterFetchingErrors map[string]error +} + +// NewListEKSClustersClient creates a new ListEKSClusters client using AWSClientRequest. +func NewListEKSClustersClient(ctx context.Context, req *AWSClientRequest) (ListEKSClustersClient, error) { + clt, err := newEKSClient(ctx, req) + return clt, trace.Wrap(err) +} + +// ListEKSClustersClient describes the required methods to List EKS clusters using a 3rd Party API. +type ListEKSClustersClient interface { + // ListClusters lists the EKS clusters. + ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) + + // DescribeCluster returns detailed information about an EKS cluster. + DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, optFns ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) +} + +// concurrentEKSFetchingLimit is a limit of how many clusters we are trying to describe concurrently, after receiving a list of clusters. +const concurrentEKSFetchingLimit = 5 + +// ListEKSClusters calls the following AWS API: +// https://docs.aws.amazon.com/eks/latest/APIReference/API_ListClusters.html - to list available EKS clusters +// https://docs.aws.amazon.com/eks/latest/APIReference/API_DescribeCluster.html - to get more detailed information about +// the each cluster in the list we received. +// It returns a list of EKS clusters with detailed information about them. +func ListEKSClusters(ctx context.Context, clt ListEKSClustersClient, req ListEKSClustersRequest) (*ListEKSClustersResponse, error) { + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + listEKSClusters := &eks.ListClustersInput{} + if req.NextToken != "" { + listEKSClusters.NextToken = &req.NextToken + } + eksClusters, err := clt.ListClusters(ctx, listEKSClusters) + if err != nil { + return nil, trace.Wrap(err) + } + + var mu sync.Mutex + ret := &ListEKSClustersResponse{ + NextToken: aws.ToString(eksClusters.NextToken), + ClusterFetchingErrors: map[string]error{}, + } + + group, groupCtx := errgroup.WithContext(ctx) + group.SetLimit(concurrentEKSFetchingLimit) + + ret.Clusters = make([]EKSCluster, 0, len(eksClusters.Clusters)) + for _, clusterName := range eksClusters.Clusters { + clusterName := clusterName + if clusterName == "" { + continue + } + group.Go(func() error { + eksClusterInfo, err := clt.DescribeCluster(groupCtx, &eks.DescribeClusterInput{ + Name: aws.String(clusterName), + }) + + mu.Lock() + defer mu.Unlock() + + if err != nil { + ret.ClusterFetchingErrors[clusterName] = err + return nil + } + + extraLabels, err := getExtraEKSLabels(eksClusterInfo.Cluster) + if err != nil { + ret.ClusterFetchingErrors[clusterName] = err + return nil + } + + ret.Clusters = append(ret.Clusters, EKSCluster{ + Name: aws.ToString(eksClusterInfo.Cluster.Name), + Region: req.Region, + Labels: eksClusterInfo.Cluster.Tags, + JoinLabels: extraLabels, + Status: strings.ToLower(string(eksClusterInfo.Cluster.Status)), + }) + return nil + }) + } + + // We don't return error from individual group goroutines, they are gathered in the returned value. + _ = group.Wait() + + return ret, nil +} + +func getExtraEKSLabels(cluster *eksTypes.Cluster) (map[string]string, error) { + parsedARN, err := arn.Parse(aws.ToString(cluster.Arn)) + if err != nil { + return nil, trace.Wrap(err) + } + return map[string]string{ + types.CloudLabel: types.CloudAWS, + types.DiscoveryLabelAccountID: parsedARN.AccountID, + types.DiscoveryLabelRegion: parsedARN.Region, + }, nil +} diff --git a/lib/integrations/awsoidc/eks_list_clusters_test.go b/lib/integrations/awsoidc/eks_list_clusters_test.go new file mode 100644 index 0000000000000..21710971454b7 --- /dev/null +++ b/lib/integrations/awsoidc/eks_list_clusters_test.go @@ -0,0 +1,265 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package awsoidc + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/eks" + eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/smithy-go/middleware" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" +) + +type mockListEKSClustersClient struct { + pageSize int + + eksClusters []eksTypes.Cluster +} + +func (m mockListEKSClustersClient) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) { + startPos := 0 + var err error + if params.NextToken != nil { + startPos, err = strconv.Atoi(*params.NextToken) + if err != nil { + return nil, err + } + } + + endPos := startPos + m.pageSize + var nextToken = aws.String(strconv.Itoa(endPos)) + if endPos > len(m.eksClusters) { + endPos = len(m.eksClusters) + nextToken = nil + } + + var clusters []string + for _, c := range m.eksClusters[startPos:endPos] { + clusters = append(clusters, *c.Name) + } + + return &eks.ListClustersOutput{ + Clusters: clusters, + NextToken: nextToken, + ResultMetadata: middleware.Metadata{}, + }, nil +} + +func (m mockListEKSClustersClient) DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, optFns ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) { + if strings.Contains(*params.Name, "error") { + return nil, errors.New(*params.Name) + } + + for _, c := range m.eksClusters { + if *c.Name == *params.Name { + return &eks.DescribeClusterOutput{ + Cluster: &c, + ResultMetadata: middleware.Metadata{}, + }, nil + } + } + + return nil, trace.NotFound("cluster not found") +} + +func TestListEKSClusters(t *testing.T) { + ctx := context.Background() + region := "us-east-1" + baseArn := "arn:aws:eks:us-east-1:880713328506:cluster/EKS" + + t.Run("pagination", func(t *testing.T) { + eksClustersAmount := 203 + allClusters := make([]eksTypes.Cluster, 0, eksClustersAmount) + + for c := 0; c < eksClustersAmount; c++ { + allClusters = append(allClusters, eksTypes.Cluster{ + Name: aws.String(fmt.Sprintf("EKS_%d", c)), + Arn: aws.String(fmt.Sprintf("%s_%d", baseArn, c)), + Tags: map[string]string{"label": "value"}, + Status: "active", + }) + } + + pageSize := 100 + mockListClient := mockListEKSClustersClient{ + pageSize: pageSize, + eksClusters: allClusters, + } + + // First call must return pageSize number of clusters from the first page. + resp, err := ListEKSClusters(ctx, mockListClient, ListEKSClustersRequest{ + Region: region, + }) + require.NoError(t, err) + require.Len(t, resp.Clusters, pageSize) + require.Regexp(t, `EKS_\d\d?`, resp.Clusters[0].Name, "EKS cluster is not from the first page") + require.NotEmpty(t, resp.NextToken) + + // Second call must also return pageSize number of clusters, from the next page. + resp, err = ListEKSClusters(ctx, mockListClient, ListEKSClustersRequest{ + Region: region, + NextToken: resp.NextToken, + }) + require.NoError(t, err) + require.Len(t, resp.Clusters, pageSize) + require.Regexp(t, `EKS_\d\d\d`, resp.Clusters[0].Name, "EKS cluster is not from the second page") + require.NotEmpty(t, resp.NextToken) + + // Third call musts return remaining amount of clusters and empty NextToken. + resp, err = ListEKSClusters(ctx, mockListClient, ListEKSClustersRequest{ + Region: region, + NextToken: resp.NextToken, + }) + require.NoError(t, err) + require.Len(t, resp.Clusters, eksClustersAmount-2*pageSize) + require.Empty(t, resp.NextToken) + }) + + testCases := []struct { + name string + inputEKSClusters []eksTypes.Cluster + expectedClusters []EKSCluster + expectedFetchingErrors map[string]error + }{ + { + name: "success, one cluster", + inputEKSClusters: []eksTypes.Cluster{ + { + Arn: aws.String(baseArn), + Name: aws.String("EKS"), + Status: "active", + Tags: map[string]string{"label": "value"}, + }, + }, + expectedClusters: []EKSCluster{ + { + Name: "EKS", + Region: region, + Labels: map[string]string{"label": "value"}, + JoinLabels: map[string]string{ + "account-id": "880713328506", + "region": "us-east-1", + "teleport.dev/cloud": "AWS", + }, + Status: "active", + }, + }, + expectedFetchingErrors: map[string]error{}, + }, + { + name: "success, two clusters", + inputEKSClusters: []eksTypes.Cluster{ + { + Arn: aws.String(baseArn), + Name: aws.String("EKS"), + Status: "active", + Tags: map[string]string{"label": "value"}, + }, + { + Arn: aws.String(baseArn + "2"), + Name: aws.String("EKS2"), + Status: "active", + Tags: map[string]string{"label2": "value2"}, + }, + }, + expectedClusters: []EKSCluster{ + { + Name: "EKS", + Region: region, + Labels: map[string]string{"label": "value"}, + JoinLabels: map[string]string{ + "account-id": "880713328506", + "region": "us-east-1", + "teleport.dev/cloud": "AWS", + }, + Status: "active", + }, + { + Name: "EKS2", + Region: region, + Labels: map[string]string{"label2": "value2"}, + JoinLabels: map[string]string{ + "account-id": "880713328506", + "region": "us-east-1", + "teleport.dev/cloud": "AWS", + }, + Status: "active", + }, + }, + expectedFetchingErrors: map[string]error{}, + }, + { + name: "two clusters, one success, one error", + inputEKSClusters: []eksTypes.Cluster{ + { + Arn: aws.String(baseArn), + Name: aws.String("EKS"), + Status: "active", + Tags: map[string]string{"label": "value"}, + }, + { + Arn: aws.String(baseArn), + Name: aws.String("erroredCluster"), + Status: "active", + Tags: map[string]string{"label2": "value2"}, + }, + }, + expectedClusters: []EKSCluster{ + { + Name: "EKS", + Region: region, + Labels: map[string]string{"label": "value"}, + JoinLabels: map[string]string{ + "account-id": "880713328506", + "region": "us-east-1", + "teleport.dev/cloud": "AWS", + }, + Status: "active", + }, + }, + expectedFetchingErrors: map[string]error{"erroredCluster": errors.New("erroredCluster")}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockListClient := mockListEKSClustersClient{ + pageSize: 100, + eksClusters: tc.inputEKSClusters, + } + + resp, err := ListEKSClusters(ctx, mockListClient, ListEKSClustersRequest{Region: region}) + require.NoError(t, err) + require.NotNil(t, resp) + require.ElementsMatch(t, tc.expectedClusters, resp.Clusters) + require.Len(t, resp.ClusterFetchingErrors, len(tc.expectedFetchingErrors)) + for clusterName := range tc.expectedFetchingErrors { + require.Equal(t, tc.expectedFetchingErrors[clusterName], resp.ClusterFetchingErrors[clusterName]) + } + }) + } +} diff --git a/lib/services/kubernetes.go b/lib/services/kubernetes.go index aadbacc813382..957035f1d6b25 100644 --- a/lib/services/kubernetes.go +++ b/lib/services/kubernetes.go @@ -24,7 +24,6 @@ import ( "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/eks" "github.com/gravitational/trace" log "github.com/sirupsen/logrus" "golang.org/x/exp/maps" @@ -264,35 +263,34 @@ func labelsFromGCPKubeCluster(cluster gcp.GKECluster) map[string]string { } // NewKubeClusterFromAWSEKS creates a kube_cluster resource from an EKS cluster. -func NewKubeClusterFromAWSEKS(cluster *eks.Cluster) (types.KubeCluster, error) { - parsedARN, err := arn.Parse(aws.StringValue(cluster.Arn)) +func NewKubeClusterFromAWSEKS(clusterName, clusterArn string, tags map[string]*string) (types.KubeCluster, error) { + parsedARN, err := arn.Parse(clusterArn) if err != nil { return nil, trace.Wrap(err) } - labels := labelsFromAWSKubeCluster(cluster, parsedARN) + labels := labelsFromAWSKubeClusterTags(tags, parsedARN) return types.NewKubernetesClusterV3( setAWSKubeName(types.Metadata{ Description: fmt.Sprintf("AWS EKS cluster %q in %s", - aws.StringValue(cluster.Name), + clusterName, parsedARN.Region), Labels: labels, - }, aws.StringValue(cluster.Name)), + }, clusterName), types.KubernetesClusterSpecV3{ AWS: types.KubeAWS{ - Name: aws.StringValue(cluster.Name), + Name: clusterName, AccountID: parsedARN.AccountID, Region: parsedARN.Region, }, }) } -// labelsFromAWSKubeCluster creates kube cluster labels. -func labelsFromAWSKubeCluster(cluster *eks.Cluster, parsedARN arn.ARN) map[string]string { - labels := awsEKSTagsToLabels(cluster.Tags) +// labelsFromAWSKubeClusterTags creates kube cluster labels. +func labelsFromAWSKubeClusterTags(tags map[string]*string, parsedARN arn.ARN) map[string]string { + labels := awsEKSTagsToLabels(tags) labels[types.CloudLabel] = types.CloudAWS labels[types.DiscoveryLabelRegion] = parsedARN.Region - labels[types.DiscoveryLabelAccountID] = parsedARN.AccountID return labels } diff --git a/lib/services/kubernetes_test.go b/lib/services/kubernetes_test.go index d1f21da074f62..9fe01893d7d12 100644 --- a/lib/services/kubernetes_test.go +++ b/lib/services/kubernetes_test.go @@ -116,7 +116,7 @@ func TestNewKubeClusterFromAWSEKS(t *testing.T) { "env": aws.String("prod"), }, } - actual, err := NewKubeClusterFromAWSEKS(cluster) + actual, err := NewKubeClusterFromAWSEKS(aws.StringValue(cluster.Name), aws.StringValue(cluster.Arn), cluster.Tags) require.NoError(t, err) require.Empty(t, cmp.Diff(expected, actual)) require.NoError(t, err) diff --git a/lib/srv/discovery/common/renaming_test.go b/lib/srv/discovery/common/renaming_test.go index e0157d3b7c5d4..6a2991ee67dd5 100644 --- a/lib/srv/discovery/common/renaming_test.go +++ b/lib/srv/discovery/common/renaming_test.go @@ -506,7 +506,7 @@ func makeEKSKubeCluster(t *testing.T, name, region, accountID, overrideLabel str overrideLabel: aws.String(name), }, } - kubeCluster, err := services.NewKubeClusterFromAWSEKS(eksCluster) + kubeCluster, err := services.NewKubeClusterFromAWSEKS(aws.StringValue(eksCluster.Name), aws.StringValue(eksCluster.Arn), eksCluster.Tags) require.NoError(t, err) require.True(t, kubeCluster.IsAWS()) return kubeCluster diff --git a/lib/srv/discovery/discovery_test.go b/lib/srv/discovery/discovery_test.go index 8932cd6668840..562862f8eb852 100644 --- a/lib/srv/discovery/discovery_test.go +++ b/lib/srv/discovery/discovery_test.go @@ -1331,7 +1331,7 @@ var eksMockClusters = []*eks.Cluster{ } func mustConvertEKSToKubeCluster(t *testing.T, eksCluster *eks.Cluster, discoveryGroup string) types.KubeCluster { - cluster, err := services.NewKubeClusterFromAWSEKS(eksCluster) + cluster, err := services.NewKubeClusterFromAWSEKS(aws.StringValue(eksCluster.Name), aws.StringValue(eksCluster.Arn), eksCluster.Tags) require.NoError(t, err) cluster.GetStaticLabels()[types.TeleportInternalDiscoveryGroupName] = discoveryGroup common.ApplyEKSNameSuffix(cluster) diff --git a/lib/srv/discovery/fetchers/eks.go b/lib/srv/discovery/fetchers/eks.go index 004fe491c2881..92cfe4051ea54 100644 --- a/lib/srv/discovery/fetchers/eks.go +++ b/lib/srv/discovery/fetchers/eks.go @@ -232,7 +232,7 @@ func (a *eksFetcher) getMatchingKubeCluster(ctx context.Context, clusterName str return nil, trace.CompareFailed("EKS cluster %q not enrolled due to its current status: %s", clusterName, st) } - cluster, err := services.NewKubeClusterFromAWSEKS(rsp.Cluster) + cluster, err := services.NewKubeClusterFromAWSEKS(aws.StringValue(rsp.Cluster.Name), aws.StringValue(rsp.Cluster.Arn), rsp.Cluster.Tags) if err != nil { return nil, trace.WrapWithMessage(err, "Unable to convert eks.Cluster cluster into types.KubernetesClusterV3.") } diff --git a/lib/srv/discovery/fetchers/eks_test.go b/lib/srv/discovery/fetchers/eks_test.go index 3c905b7af5fcb..b28dde46f16c3 100644 --- a/lib/srv/discovery/fetchers/eks_test.go +++ b/lib/srv/discovery/fetchers/eks_test.go @@ -204,7 +204,7 @@ var eksMockClusters = []*eks.Cluster{ func eksClustersToResources(t *testing.T, clusters ...*eks.Cluster) types.ResourcesWithLabels { var kubeClusters types.KubeClusters for _, cluster := range clusters { - kubeCluster, err := services.NewKubeClusterFromAWSEKS(cluster) + kubeCluster, err := services.NewKubeClusterFromAWSEKS(aws.StringValue(cluster.Name), aws.StringValue(cluster.Arn), cluster.Tags) require.NoError(t, err) require.True(t, kubeCluster.IsAWS()) common.ApplyEKSNameSuffix(kubeCluster) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 90766c376e6b2..5f1aa5622ef8d 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -844,6 +844,7 @@ func (h *Handler) bindDefaultEndpoints() { h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/deploydatabaseservices", h.WithClusterAuth(h.awsOIDCDeployDatabaseServices)) h.GET("/webapi/scripts/integrations/configure/deployservice-iam.sh", h.WithLimiter(h.awsOIDCConfigureDeployServiceIAM)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/ec2", h.WithClusterAuth(h.awsOIDCListEC2)) + h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/eksclusters", h.WithClusterAuth(h.awsOIDCListEKSClusters)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/ec2ice", h.WithClusterAuth(h.awsOIDCListEC2ICE)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/deployec2ice", h.WithClusterAuth(h.awsOIDCDeployEC2ICE)) h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/securitygroups", h.WithClusterAuth(h.awsOIDCListSecurityGroups)) diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index 3a625e2dab000..a36bebff80c41 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -392,6 +392,42 @@ func (h *Handler) awsOIDCConfigureEKSIAM(w http.ResponseWriter, r *http.Request, return nil, trace.Wrap(err) } +// awsOIDCListEKSClusters returns a list of EKS clusters using the ListEKSClusters action of the AWS OIDC integration. +func (h *Handler) awsOIDCListEKSClusters(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (any, error) { + ctx := r.Context() + + var req ui.AWSOIDCListEKSClustersRequest + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + + awsClientReq, err := h.awsOIDCClientRequest(ctx, req.Region, p, sctx, site) + if err != nil { + return nil, trace.Wrap(err) + } + + listClient, err := awsoidc.NewListEKSClustersClient(ctx, awsClientReq) + if err != nil { + return nil, trace.Wrap(err) + } + + resp, err := awsoidc.ListEKSClusters(ctx, + listClient, + awsoidc.ListEKSClustersRequest{ + Region: req.Region, + NextToken: req.NextToken, + }, + ) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.AWSOIDCListEKSClustersResponse{ + NextToken: resp.NextToken, + Clusters: ui.MakeEKSClusters(resp.Clusters), + }, nil +} + // awsOIDCListEC2 returns a list of EC2 Instances using the ListEC2 action of the AWS OIDC Integration. func (h *Handler) awsOIDCListEC2(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (any, error) { ctx := r.Context() diff --git a/lib/web/ui/integration.go b/lib/web/ui/integration.go index 6083502c3dcb4..bc21d86b017f0 100644 --- a/lib/web/ui/integration.go +++ b/lib/web/ui/integration.go @@ -234,6 +234,25 @@ type AWSOIDCDeployDatabaseServiceResponse struct { ClusterDashboardURL string `json:"clusterDashboardUrl"` } +// AWSOIDCListEKSClustersRequest is a request to ListEKSClusters using the AWS OIDC Integration. +type AWSOIDCListEKSClustersRequest struct { + // Region is the AWS Region. + Region string `json:"region"` + // NextToken is the token to be used to fetch the next page. + // If empty, the first page is fetched. + NextToken string `json:"nextToken"` +} + +// AWSOIDCListEKSClustersResponse contains a list of clusters and a next token if more pages are available. +type AWSOIDCListEKSClustersResponse struct { + // Clusters contains the page with list of EKSCluster + Clusters []EKSCluster `json:"clusters"` + + // NextToken is used for pagination. + // If non-empty, it can be used to request the next page. + NextToken string `json:"nextToken,omitempty"` +} + // AWSOIDCListEC2Request is a request to ListEC2s using the AWS OIDC Integration. type AWSOIDCListEC2Request struct { // Region is the AWS Region. diff --git a/lib/web/ui/server.go b/lib/web/ui/server.go index c23c214aa493f..90fa7fe3bcdd5 100644 --- a/lib/web/ui/server.go +++ b/lib/web/ui/server.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/integrations/awsoidc" "github.com/gravitational/teleport/lib/services" ) @@ -157,6 +158,15 @@ func MakeServers(clusterName string, servers []types.Server, accessChecker servi return uiServers, nil } +// EKSCluster represents and EKS cluster, analog of awsoidc.EKSCluster, but used by web ui. +type EKSCluster struct { + Name string `json:"name"` + Region string `json:"region"` + Labels []Label `json:"labels"` + JoinLabels []Label `json:"joinLabels"` + Status string `json:"status"` +} + // KubeCluster describes a kube cluster. type KubeCluster struct { // Kind is the kind of resource. Used to parse which kind in a list of unified resources in the UI @@ -186,6 +196,22 @@ func MakeKubeCluster(cluster types.KubeCluster, accessChecker services.AccessChe } } +// MakeEKSClusters creates EKS objects for the web UI. +func MakeEKSClusters(clusters []awsoidc.EKSCluster) []EKSCluster { + uiEKSClusters := make([]EKSCluster, 0, len(clusters)) + + for _, cluster := range clusters { + uiEKSClusters = append(uiEKSClusters, EKSCluster{ + Name: cluster.Name, + Region: cluster.Region, + Labels: makeLabels(cluster.Labels), + JoinLabels: makeLabels(cluster.JoinLabels), + Status: cluster.Status, + }) + } + return uiEKSClusters +} + // MakeKubeClusters creates ui kube objects and returns a list. func MakeKubeClusters(clusters []types.KubeCluster, accessChecker services.AccessChecker) []KubeCluster { uiKubeClusters := make([]KubeCluster, 0, len(clusters))