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))