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:
Marco André Dinis
2023-09-14 14:15:43 +00:00
committed by GitHub
parent 822ab9306e
commit 524f8ed8b7
4 changed files with 143 additions and 18 deletions
+33 -1
View File
@@ -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
View File
@@ -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
}
+49 -2
View File
@@ -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.