Skip to content

Commit

Permalink
[v17] AWS OIDC: List Deployed Database Services - implementation (#50374
Browse files Browse the repository at this point in the history
)

* AWS OIDC: List Deployed Database Services - implementation

This PR implements the List Deployed Database Services.

This will be used to let the user know which deployed database services
were deployed during the Enroll New Resource / RDS flows.

* validate region for dashboard url
  • Loading branch information
marcoandredinis authored Dec 19, 2024
1 parent 9420f6a commit 0ff9480
Show file tree
Hide file tree
Showing 5 changed files with 628 additions and 3 deletions.
52 changes: 52 additions & 0 deletions lib/auth/integration/integrationv1/awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,58 @@ func (s *AWSOIDCService) DeployDatabaseService(ctx context.Context, req *integra
}, nil
}

// ListDeployedDatabaseServices lists Database Services deployed into Amazon ECS.
func (s *AWSOIDCService) ListDeployedDatabaseServices(ctx context.Context, req *integrationpb.ListDeployedDatabaseServicesRequest) (*integrationpb.ListDeployedDatabaseServicesResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.CheckAccessToKind(types.KindIntegration, types.VerbUse); err != nil {
return nil, trace.Wrap(err)
}

clusterName, err := s.cache.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
}

awsClientReq, err := s.awsClientReq(ctx, req.Integration, req.Region)
if err != nil {
return nil, trace.Wrap(err)
}

listDatabaseServicesClient, err := awsoidc.NewListDeployedDatabaseServicesClient(ctx, awsClientReq)
if err != nil {
return nil, trace.Wrap(err)
}

listDatabaseServicesResponse, err := awsoidc.ListDeployedDatabaseServices(ctx, listDatabaseServicesClient, awsoidc.ListDeployedDatabaseServicesRequest{
Integration: req.Integration,
TeleportClusterName: clusterName.GetClusterName(),
Region: req.Region,
NextToken: req.NextToken,
})
if err != nil {
return nil, trace.Wrap(err)
}

deployedDatabaseServices := make([]*integrationpb.DeployedDatabaseService, 0, len(listDatabaseServicesResponse.DeployedDatabaseServices))
for _, deployedService := range listDatabaseServicesResponse.DeployedDatabaseServices {
deployedDatabaseServices = append(deployedDatabaseServices, &integrationpb.DeployedDatabaseService{
Name: deployedService.Name,
ServiceDashboardUrl: deployedService.ServiceDashboardURL,
ContainerEntryPoint: deployedService.ContainerEntryPoint,
ContainerCommand: deployedService.ContainerCommand,
})
}

return &integrationpb.ListDeployedDatabaseServicesResponse{
DeployedDatabaseServices: deployedDatabaseServices,
NextToken: listDatabaseServicesResponse.NextToken,
}, nil
}

// EnrollEKSClusters enrolls EKS clusters into Teleport by installing teleport-kube-agent chart on the clusters.
func (s *AWSOIDCService) EnrollEKSClusters(ctx context.Context, req *integrationpb.EnrollEKSClustersRequest) (*integrationpb.EnrollEKSClustersResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
Expand Down
10 changes: 10 additions & 0 deletions lib/auth/integration/integrationv1/awsoidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,16 @@ func TestRBAC(t *testing.T) {
return err
},
},
{
name: "ListDeployedDatabaseServices",
fn: func() error {
_, err := awsoidService.ListDeployedDatabaseServices(userCtx, &integrationv1.ListDeployedDatabaseServicesRequest{
Integration: integrationName,
Region: "my-region",
})
return err
},
},
} {
t.Run(tt.name, func(t *testing.T) {
err := tt.fn()
Expand Down
15 changes: 12 additions & 3 deletions lib/integrations/awsoidc/deployservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
apiaws "github.com/gravitational/teleport/api/utils/aws"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib/integrations/awsoidc/tags"
"github.com/gravitational/teleport/lib/utils/teleportassets"
Expand Down Expand Up @@ -445,16 +446,24 @@ func DeployService(ctx context.Context, clt DeployServiceClient, req DeployServi
return nil, trace.Wrap(err)
}

serviceDashboardURL := fmt.Sprintf("https://%s.console.aws.amazon.com/ecs/v2/clusters/%s/services/%s", req.Region, aws.ToString(req.ClusterName), aws.ToString(req.ServiceName))

return &DeployServiceResponse{
ClusterARN: aws.ToString(cluster.ClusterArn),
ServiceARN: aws.ToString(service.ServiceArn),
TaskDefinitionARN: taskDefinitionARN,
ServiceDashboardURL: serviceDashboardURL,
ServiceDashboardURL: serviceDashboardURL(req.Region, aws.ToString(req.ClusterName), aws.ToString(service.ServiceName)),
}, nil
}

// serviceDashboardURL builds the ECS Service dashboard URL using the AWS Region, the ECS Cluster and Service Names.
// It returns an empty string if region is not valid.
func serviceDashboardURL(region, clusterName, serviceName string) string {
if err := apiaws.IsValidRegion(region); err != nil {
return ""
}

return fmt.Sprintf("https://%s.console.aws.amazon.com/ecs/v2/clusters/%s/services/%s", region, clusterName, serviceName)
}

type upsertTaskRequest struct {
TaskName string
TaskRoleARN string
Expand Down
194 changes: 194 additions & 0 deletions lib/integrations/awsoidc/listdeployeddatabaseservice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

package awsoidc

import (
"context"
"log/slog"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecs"
ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/gravitational/trace"

"github.com/gravitational/teleport/lib/integrations/awsoidc/tags"
)

// ListDeployedDatabaseServicesRequest contains the required fields to list the deployed database services in Amazon ECS.
type ListDeployedDatabaseServicesRequest struct {
// Region is the AWS Region.
Region string
// Integration is the AWS OIDC Integration name
Integration string
// TeleportClusterName is the name of the Teleport Cluster.
// Used to uniquely identify the ECS Cluster in Amazon.
TeleportClusterName string
// NextToken is the token to be used to fetch the next page.
// If empty, the first page is fetched.
NextToken string
}

func (req *ListDeployedDatabaseServicesRequest) checkAndSetDefaults() error {
if req.Region == "" {
return trace.BadParameter("region is required")
}

if req.Integration == "" {
return trace.BadParameter("integration is required")
}

if req.TeleportClusterName == "" {
return trace.BadParameter("teleport cluster name is required")
}

return nil
}

// ListDeployedDatabaseServicesResponse contains a page of Deployed Database Services.
type ListDeployedDatabaseServicesResponse struct {
// DeployedDatabaseServices contains the page of Deployed Database Services.
DeployedDatabaseServices []DeployedDatabaseService `json:"deployedDatabaseServices"`

// NextToken is used for pagination.
// If non-empty, it can be used to request the next page.
NextToken string `json:"nextToken"`
}

// DeployedDatabaseService contains a database service that was deployed to Amazon ECS.
type DeployedDatabaseService struct {
// Name is the ECS Service name.
Name string
// ServiceDashboardURL is the Amazon Web Console URL for this ECS Service.
ServiceDashboardURL string
// ContainerEntryPoint is the entry point for the container 0 that is running in the ECS Task.
ContainerEntryPoint []string
// ContainerCommand is the list of arguments that are passed into the ContainerEntryPoint.
ContainerCommand []string
}

// ListDeployedDatabaseServicesClient describes the required methods to list AWS VPCs.
type ListDeployedDatabaseServicesClient interface {
// ListServices returns a list of services.
ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error)
// DescribeServices returns ECS Services details.
DescribeServices(ctx context.Context, params *ecs.DescribeServicesInput, optFns ...func(*ecs.Options)) (*ecs.DescribeServicesOutput, error)
// DescribeTaskDefinition returns an ECS Task Definition.
DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}

type defaultListDeployedDatabaseServicesClient struct {
*ecs.Client
}

// NewListDeployedDatabaseServicesClient creates a new ListDeployedDatabaseServicesClient using an AWSClientRequest.
func NewListDeployedDatabaseServicesClient(ctx context.Context, req *AWSClientRequest) (ListDeployedDatabaseServicesClient, error) {
ecsClient, err := newECSClient(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}

return &defaultListDeployedDatabaseServicesClient{
Client: ecsClient,
}, nil
}

// ListDeployedDatabaseServices calls the following AWS API:
// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ListServices.html
// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeServices.html
// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTaskDefinition.html
// It returns a list of ECS Services running Teleport Database Service and an optional NextToken that can be used to fetch the next page.
func ListDeployedDatabaseServices(ctx context.Context, clt ListDeployedDatabaseServicesClient, req ListDeployedDatabaseServicesRequest) (*ListDeployedDatabaseServicesResponse, error) {
if err := req.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}

clusterName := normalizeECSClusterName(req.TeleportClusterName)

log := slog.With(
"integration", req.Integration,
"aws_region", req.Region,
"ecs_cluster", clusterName,
)

// Do not increase this value because ecs.DescribeServices only allows up to 10 services per API call.
maxServicesPerPage := aws.Int32(10)
listServicesInput := &ecs.ListServicesInput{
Cluster: &clusterName,
MaxResults: maxServicesPerPage,
LaunchType: ecstypes.LaunchTypeFargate,
}
if req.NextToken != "" {
listServicesInput.NextToken = &req.NextToken
}

listServicesOutput, err := clt.ListServices(ctx, listServicesInput)
if err != nil {
return nil, trace.Wrap(err)
}

describeServicesOutput, err := clt.DescribeServices(ctx, &ecs.DescribeServicesInput{
Services: listServicesOutput.ServiceArns,
Include: []ecstypes.ServiceField{ecstypes.ServiceFieldTags},
Cluster: &clusterName,
})
if err != nil {
return nil, trace.Wrap(err)
}

ownershipTags := tags.DefaultResourceCreationTags(req.TeleportClusterName, req.Integration)

deployedDatabaseServices := []DeployedDatabaseService{}
for _, ecsService := range describeServicesOutput.Services {
log := log.With("ecs_service", aws.ToString(ecsService.ServiceName))
if !ownershipTags.MatchesECSTags(ecsService.Tags) {
log.WarnContext(ctx, "Missing ownership tags in ECS Service, skipping")
continue
}

taskDefinitionOut, err := clt.DescribeTaskDefinition(ctx, &ecs.DescribeTaskDefinitionInput{
TaskDefinition: ecsService.TaskDefinition,
})
if err != nil {
return nil, trace.Wrap(err)
}

if len(taskDefinitionOut.TaskDefinition.ContainerDefinitions) == 0 {
log.WarnContext(ctx, "Task has no containers defined, skipping",
"ecs_task_family", aws.ToString(taskDefinitionOut.TaskDefinition.Family),
"ecs_task_revision", taskDefinitionOut.TaskDefinition.Revision,
)
continue
}

entryPoint := taskDefinitionOut.TaskDefinition.ContainerDefinitions[0].EntryPoint
command := taskDefinitionOut.TaskDefinition.ContainerDefinitions[0].Command

deployedDatabaseServices = append(deployedDatabaseServices, DeployedDatabaseService{
Name: aws.ToString(ecsService.ServiceName),
ServiceDashboardURL: serviceDashboardURL(req.Region, clusterName, aws.ToString(ecsService.ServiceName)),
ContainerEntryPoint: entryPoint,
ContainerCommand: command,
})
}

return &ListDeployedDatabaseServicesResponse{
DeployedDatabaseServices: deployedDatabaseServices,
NextToken: aws.ToString(listServicesOutput.NextToken),
}, nil
}
Loading

0 comments on commit 0ff9480

Please sign in to comment.