mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
AWS OIDC - List RDS: add Subnet and VPC for aurora clusters (#31584)
* AWS OIDC - List RDS: add Subnet and VPC for aurora clusters When converting from an Aurora Cluster to a Teleport Database we were not populating the VPC and Subnets. This PR adds those fields using one of the instances for the cluster. * review pt1 * improve comment
This commit is contained in:
@@ -19,6 +19,7 @@ package awsoidc
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/rds"
|
||||
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
|
||||
"github.com/gravitational/trace"
|
||||
@@ -30,6 +31,9 @@ import (
|
||||
var (
|
||||
// filterEngine is the filter name for filtering Databses based on their engine.
|
||||
filterEngine = "engine"
|
||||
|
||||
// filterDBClusterID is the filter name for filtering RDS Instances for a given RDS Cluster.
|
||||
filterDBClusterID = "db-cluster-id"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -188,7 +192,16 @@ func listDBClusters(ctx context.Context, clt ListDatabasesClient, req ListDataba
|
||||
continue
|
||||
}
|
||||
|
||||
awsDB, err := services.NewDatabaseFromRDSV2Cluster(&db)
|
||||
// RDS Clusters do not return VPC and Subnets.
|
||||
// To get this value, a member of the cluster is fetched and its Network Information is used to
|
||||
// populate the RDS Cluster information.
|
||||
// All the members have the same network information, so picking one at random should not matter.
|
||||
clusterInstance, err := fetchSingleRDSDBInstance(ctx, clt, req, aws.ToString(db.DBClusterIdentifier))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
awsDB, err := services.NewDatabaseFromRDSV2Cluster(&db, clusterInstance)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -198,3 +211,22 @@ func listDBClusters(ctx context.Context, clt ListDatabasesClient, req ListDataba
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func fetchSingleRDSDBInstance(ctx context.Context, clt ListDatabasesClient, req ListDatabasesRequest, clusterID string) (*rdsTypes.DBInstance, error) {
|
||||
describeDBInstanceInput := &rds.DescribeDBInstancesInput{
|
||||
Filters: []rdsTypes.Filter{
|
||||
{Name: &filterDBClusterID, Values: []string{clusterID}},
|
||||
},
|
||||
}
|
||||
|
||||
rdsDBs, err := clt.DescribeDBInstances(ctx, describeDBInstanceInput)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
if len(rdsDBs.DBInstances) == 0 {
|
||||
return nil, trace.BadParameter("database cluster %s has no instance", clusterID)
|
||||
}
|
||||
|
||||
return &rdsDBs.DBInstances[0], nil
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/rds"
|
||||
rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -295,6 +296,12 @@ func TestListDatabases(t *testing.T) {
|
||||
Port: &clusterPort,
|
||||
DBClusterArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"),
|
||||
}},
|
||||
mockInstances: []rdsTypes.DBInstance{{
|
||||
DBSubnetGroup: &rdsTypes.DBSubnetGroup{
|
||||
Subnets: []rdsTypes.Subnet{{SubnetIdentifier: aws.String("subnet-999")}},
|
||||
VpcId: aws.String("vpc-999"),
|
||||
},
|
||||
}},
|
||||
respCheck: func(t *testing.T, ldr *ListDatabasesResponse) {
|
||||
require.Len(t, ldr.Databases, 1, "expected 1 database, got %d", len(ldr.Databases))
|
||||
require.Empty(t, ldr.NextToken, "expected an empty NextToken")
|
||||
@@ -321,6 +328,8 @@ func TestListDatabases(t *testing.T) {
|
||||
ClusterID: "my-dbc",
|
||||
InstanceID: "aurora-instance-1",
|
||||
ResourceID: "db-123",
|
||||
Subnets: []string{"subnet-999"},
|
||||
VPCID: "vpc-999",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -330,6 +339,26 @@ func TestListDatabases(t *testing.T) {
|
||||
},
|
||||
errCheck: noErrorFunc,
|
||||
},
|
||||
|
||||
{
|
||||
name: "cluster exists but no instance exists, returns an error",
|
||||
req: ListDatabasesRequest{
|
||||
Region: "us-east-1",
|
||||
RDSType: "cluster",
|
||||
Engines: []string{"postgres"},
|
||||
NextToken: "",
|
||||
},
|
||||
mockClusters: []rdsTypes.DBCluster{{
|
||||
Status: stringPointer("available"),
|
||||
DBClusterIdentifier: stringPointer("my-dbc"),
|
||||
DbClusterResourceId: stringPointer("db-123"),
|
||||
Engine: stringPointer("aurora-postgresql"),
|
||||
Endpoint: stringPointer("aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com"),
|
||||
Port: &clusterPort,
|
||||
DBClusterArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"),
|
||||
}},
|
||||
errCheck: trace.IsBadParameter,
|
||||
},
|
||||
{
|
||||
name: "no region",
|
||||
req: ListDatabasesRequest{
|
||||
|
||||
+32
-15
@@ -620,18 +620,7 @@ func MetadataFromRDSV2Instance(rdsInstance *rdsTypesV2.DBInstance) (*types.AWS,
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var vpcID string
|
||||
var subnets []string
|
||||
if rdsInstance.DBSubnetGroup != nil {
|
||||
vpcID = aws.StringValue(rdsInstance.DBSubnetGroup.VpcId)
|
||||
subnets = make([]string, 0, len(rdsInstance.DBSubnetGroup.Subnets))
|
||||
for _, s := range rdsInstance.DBSubnetGroup.Subnets {
|
||||
if s.SubnetIdentifier == nil || *s.SubnetIdentifier == "" {
|
||||
continue
|
||||
}
|
||||
subnets = append(subnets, *s.SubnetIdentifier)
|
||||
}
|
||||
}
|
||||
vpcID, subnets := rdsSubnetGroupToNetworkInfo(rdsInstance.DBSubnetGroup)
|
||||
|
||||
return &types.AWS{
|
||||
Region: parsedARN.Region,
|
||||
@@ -660,8 +649,8 @@ func labelsFromRDSV2Instance(rdsInstance *rdsTypesV2.DBInstance, meta *types.AWS
|
||||
|
||||
// NewDatabaseFromRDSV2Cluster creates a database resource from an RDS cluster (Aurora).
|
||||
// It uses aws sdk v2.
|
||||
func NewDatabaseFromRDSV2Cluster(cluster *rdsTypesV2.DBCluster) (types.Database, error) {
|
||||
metadata, err := MetadataFromRDSV2Cluster(cluster)
|
||||
func NewDatabaseFromRDSV2Cluster(cluster *rdsTypesV2.DBCluster, firstInstance *rdsTypesV2.DBInstance) (types.Database, error) {
|
||||
metadata, err := MetadataFromRDSV2Cluster(cluster, firstInstance)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -686,13 +675,39 @@ func NewDatabaseFromRDSV2Cluster(cluster *rdsTypesV2.DBCluster) (types.Database,
|
||||
})
|
||||
}
|
||||
|
||||
func rdsSubnetGroupToNetworkInfo(subnetGroup *rdsTypesV2.DBSubnetGroup) (vpcID string, subnets []string) {
|
||||
if subnetGroup == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vpcID = aws.StringValue(subnetGroup.VpcId)
|
||||
subnets = make([]string, 0, len(subnetGroup.Subnets))
|
||||
for _, s := range subnetGroup.Subnets {
|
||||
subnetID := aws.StringValue(s.SubnetIdentifier)
|
||||
if subnetID != "" {
|
||||
subnets = append(subnets, subnetID)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MetadataFromRDSV2Cluster creates AWS metadata from the provided RDS cluster.
|
||||
// It uses aws sdk v2.
|
||||
func MetadataFromRDSV2Cluster(rdsCluster *rdsTypesV2.DBCluster) (*types.AWS, error) {
|
||||
// An optional [rdsTypesV2.DBInstance] can be passed to fill the network configuration of the Cluster.
|
||||
func MetadataFromRDSV2Cluster(rdsCluster *rdsTypesV2.DBCluster, rdsInstance *rdsTypesV2.DBInstance) (*types.AWS, error) {
|
||||
parsedARN, err := arn.Parse(aws.StringValue(rdsCluster.DBClusterArn))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var vpcID string
|
||||
var subnets []string
|
||||
|
||||
if rdsInstance != nil {
|
||||
vpcID, subnets = rdsSubnetGroupToNetworkInfo(rdsInstance.DBSubnetGroup)
|
||||
}
|
||||
|
||||
return &types.AWS{
|
||||
Region: parsedARN.Region,
|
||||
AccountID: parsedARN.AccountID,
|
||||
@@ -700,6 +715,8 @@ func MetadataFromRDSV2Cluster(rdsCluster *rdsTypesV2.DBCluster) (*types.AWS, err
|
||||
ClusterID: aws.StringValue(rdsCluster.DBClusterIdentifier),
|
||||
ResourceID: aws.StringValue(rdsCluster.DbClusterResourceId),
|
||||
IAMAuth: aws.BoolValue(rdsCluster.IAMDatabaseAuthenticationEnabled),
|
||||
Subnets: subnets,
|
||||
VPCID: vpcID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,7 @@ func TestDatabaseFromRDSV2Cluster(t *testing.T) {
|
||||
AWS: expectedAWS,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
actual, err := NewDatabaseFromRDSV2Cluster(cluster)
|
||||
actual, err := NewDatabaseFromRDSV2Cluster(cluster, nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(expected, actual))
|
||||
|
||||
@@ -1074,12 +1074,59 @@ func TestDatabaseFromRDSV2Cluster(t *testing.T) {
|
||||
)
|
||||
expected.Metadata.Name = newName
|
||||
|
||||
actual, err := NewDatabaseFromRDSV2Cluster(cluster)
|
||||
actual, err := NewDatabaseFromRDSV2Cluster(cluster, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, actual.GetName(), newName)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DB Cluster uses network information from DB Instance when available", func(t *testing.T) {
|
||||
instance := &rdsTypesV2.DBInstance{
|
||||
DBSubnetGroup: &rdsTypesV2.DBSubnetGroup{
|
||||
VpcId: aws.String("vpc-123"),
|
||||
Subnets: []rdsTypesV2.Subnet{
|
||||
{SubnetIdentifier: aws.String("subnet-123")},
|
||||
{SubnetIdentifier: aws.String("subnet-456")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected, err := types.NewDatabaseV3(types.Metadata{
|
||||
Name: "override-1",
|
||||
Description: "Aurora cluster in us-east-1",
|
||||
Labels: map[string]string{
|
||||
"TeleportDatabaseName": "override-1",
|
||||
"teleport.dev/database_name": "override-1",
|
||||
types.DiscoveryLabelAccountID: "123456789012",
|
||||
types.CloudLabel: types.CloudAWS,
|
||||
types.DiscoveryLabelRegion: "us-east-1",
|
||||
types.DiscoveryLabelEngine: RDSEngineAuroraMySQL,
|
||||
types.DiscoveryLabelEngineVersion: "8.0.0",
|
||||
types.DiscoveryLabelEndpointType: "primary",
|
||||
types.DiscoveryLabelStatus: "available",
|
||||
"key": "val",
|
||||
},
|
||||
}, types.DatabaseSpecV3{
|
||||
Protocol: defaults.ProtocolMySQL,
|
||||
URI: "localhost:3306",
|
||||
AWS: types.AWS{
|
||||
AccountID: "123456789012",
|
||||
Region: "us-east-1",
|
||||
RDS: types.RDS{
|
||||
ClusterID: "cluster-1",
|
||||
ResourceID: "resource-1",
|
||||
IAMAuth: true,
|
||||
Subnets: []string{"subnet-123", "subnet-456"},
|
||||
VPCID: "vpc-123",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
actual, err := NewDatabaseFromRDSV2Cluster(cluster, instance)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cmp.Diff(expected, actual), "NewDatabaseFromRDSV2Cluster diff (-want +got)")
|
||||
})
|
||||
}
|
||||
|
||||
// TestDatabaseFromRDSClusterNameOverride tests converting an RDS cluster to a database resource with overridden name.
|
||||
|
||||
Reference in New Issue
Block a user