Files
teleport/lib/srv/db/cloud/meta.go
T

407 lines
16 KiB
Go

/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cloud
import (
"context"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/elasticache/elasticacheiface"
"github.com/aws/aws-sdk-go/service/memorydb"
"github.com/aws/aws-sdk-go/service/memorydb/memorydbiface"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/rds/rdsiface"
"github.com/aws/aws-sdk-go/service/redshift"
"github.com/aws/aws-sdk-go/service/redshift/redshiftiface"
"github.com/aws/aws-sdk-go/service/redshiftserverless"
"github.com/aws/aws-sdk-go/service/redshiftserverless/redshiftserverlessiface"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/db/common"
)
// MetadataConfig is the cloud metadata service config.
type MetadataConfig struct {
// Clients is an interface for retrieving cloud clients.
Clients cloud.Clients
}
// Check validates the metadata service config.
func (c *MetadataConfig) Check() error {
if c.Clients == nil {
c.Clients = cloud.NewClients()
}
return nil
}
// Metadata is a service that fetches cloud databases metadata.
type Metadata struct {
cfg MetadataConfig
log logrus.FieldLogger
}
// NewMetadata returns a new cloud metadata service.
func NewMetadata(config MetadataConfig) (*Metadata, error) {
if err := config.Check(); err != nil {
return nil, trace.Wrap(err)
}
return &Metadata{
cfg: config,
log: logrus.WithField(trace.Component, "meta"),
}, nil
}
// Update updates cloud metadata of the provided database.
func (m *Metadata) Update(ctx context.Context, database types.Database) error {
switch database.GetType() {
case types.DatabaseTypeRDS:
return m.updateAWS(ctx, database, m.fetchRDSMetadata)
case types.DatabaseTypeRDSProxy:
return m.updateAWS(ctx, database, m.fetchRDSProxyMetadata)
case types.DatabaseTypeRedshift:
return m.updateAWS(ctx, database, m.fetchRedshiftMetadata)
case types.DatabaseTypeRedshiftServerless:
return m.updateAWS(ctx, database, m.fetchRedshiftServerlessMetadata)
case types.DatabaseTypeElastiCache:
return m.updateAWS(ctx, database, m.fetchElastiCacheMetadata)
case types.DatabaseTypeMemoryDB:
return m.updateAWS(ctx, database, m.fetchMemoryDBMetadata)
}
return nil
}
// updateAWS updates cloud metadata of the provided AWS database.
func (m *Metadata) updateAWS(ctx context.Context, database types.Database, fetchFn func(context.Context, types.Database) (*types.AWS, error)) error {
metadata, err := fetchFn(ctx, database)
if err != nil {
if trace.IsAccessDenied(err) { // Permission errors are expected.
m.log.WithError(err).Debugf("No permissions to fetch metadata for %q.", database)
return nil
}
return trace.Wrap(err)
}
m.log.Debugf("Fetched metadata for %q: %v.", database, metadata)
database.SetStatusAWS(*metadata)
return nil
}
// fetchRDSMetadata fetches metadata for the provided RDS or Aurora database.
func (m *Metadata) fetchRDSMetadata(ctx context.Context, database types.Database) (*types.AWS, error) {
rds, err := m.cfg.Clients.GetAWSRDSClient(database.GetAWS().Region)
if err != nil {
return nil, trace.Wrap(err)
}
if database.GetAWS().RDS.ClusterID != "" {
return fetchRDSClusterMetadata(ctx, rds, database.GetAWS().RDS.ClusterID)
}
// Try to fetch the RDS instance metadata.
metadata, err := fetchRDSInstanceMetadata(ctx, rds, database.GetAWS().RDS.InstanceID)
if err != nil && !trace.IsNotFound(err) && !trace.IsAccessDenied(err) {
return nil, trace.Wrap(err)
}
// If RDS instance metadata wasn't found, it may be an Aurora cluster.
if metadata == nil {
// Aurora cluster ID may be either explicitly specified or parsed
// from endpoint in which case it will be in InstanceID field.
clusterID := database.GetAWS().RDS.ClusterID
if clusterID == "" {
clusterID = database.GetAWS().RDS.InstanceID
}
return fetchRDSClusterMetadata(ctx, rds, clusterID)
}
// If instance was found, it may be a part of an Aurora cluster.
if metadata.RDS.ClusterID != "" {
return fetchRDSClusterMetadata(ctx, rds, metadata.RDS.ClusterID)
}
return metadata, nil
}
// fetchRDSProxyMetadata fetches metadata for the provided RDS Proxy database.
func (m *Metadata) fetchRDSProxyMetadata(ctx context.Context, database types.Database) (*types.AWS, error) {
rds, err := m.cfg.Clients.GetAWSRDSClient(database.GetAWS().Region)
if err != nil {
return nil, trace.Wrap(err)
}
if database.GetAWS().RDSProxy.CustomEndpointName != "" {
return fetchRDSProxyCustomEndpointMetadata(ctx, rds, database.GetAWS().RDSProxy.CustomEndpointName, database.GetURI())
}
return fetchRDSProxyMetadata(ctx, rds, database.GetAWS().RDSProxy.Name)
}
// fetchRedshiftMetadata fetches metadata for the provided Redshift database.
func (m *Metadata) fetchRedshiftMetadata(ctx context.Context, database types.Database) (*types.AWS, error) {
redshift, err := m.cfg.Clients.GetAWSRedshiftClient(database.GetAWS().Region)
if err != nil {
return nil, trace.Wrap(err)
}
cluster, err := describeRedshiftCluster(ctx, redshift, database.GetAWS().Redshift.ClusterID)
if err != nil {
return nil, trace.Wrap(err)
}
parsedARN, err := arn.Parse(aws.StringValue(cluster.ClusterNamespaceArn))
if err != nil {
return nil, trace.Wrap(err)
}
return &types.AWS{
Region: parsedARN.Region,
AccountID: parsedARN.AccountID,
Redshift: types.Redshift{
ClusterID: aws.StringValue(cluster.ClusterIdentifier),
},
}, nil
}
// fetchRedshiftServerlessMetadata fetches metadata for the provided Redshift
// Serverless database.
func (m *Metadata) fetchRedshiftServerlessMetadata(ctx context.Context, database types.Database) (*types.AWS, error) {
meta := database.GetAWS()
client, err := m.cfg.Clients.GetAWSRedshiftServerlessClient(meta.Region)
if err != nil {
return nil, trace.Wrap(err)
}
if meta.RedshiftServerless.EndpointName != "" {
return fetchRedshiftServerlessVPCEndpointMetadata(ctx, client, meta.RedshiftServerless.EndpointName)
}
return fetchRedshiftServerlessWorkgroupMetadata(ctx, client, meta.RedshiftServerless.WorkgroupName)
}
// fetchElastiCacheMetadata fetches metadata for the provided ElastiCache database.
func (m *Metadata) fetchElastiCacheMetadata(ctx context.Context, database types.Database) (*types.AWS, error) {
elastiCacheClient, err := m.cfg.Clients.GetAWSElastiCacheClient(database.GetAWS().Region)
if err != nil {
return nil, trace.Wrap(err)
}
cluster, err := describeElastiCacheCluster(ctx, elastiCacheClient, database.GetAWS().ElastiCache.ReplicationGroupID)
if err != nil {
return nil, trace.Wrap(err)
}
// Endpoint type does not change.
endpointType := database.GetAWS().ElastiCache.EndpointType
return services.MetadataFromElastiCacheCluster(cluster, endpointType)
}
// fetchMemoryDBMetadata fetches metadata for the provided MemoryDB database.
func (m *Metadata) fetchMemoryDBMetadata(ctx context.Context, database types.Database) (*types.AWS, error) {
memoryDBClient, err := m.cfg.Clients.GetAWSMemoryDBClient(database.GetAWS().Region)
if err != nil {
return nil, trace.Wrap(err)
}
cluster, err := describeMemoryDBCluster(ctx, memoryDBClient, database.GetAWS().MemoryDB.ClusterName)
if err != nil {
return nil, trace.Wrap(err)
}
// Endpoint type does not change.
endpointType := database.GetAWS().MemoryDB.EndpointType
return services.MetadataFromMemoryDBCluster(cluster, endpointType)
}
// fetchRDSInstanceMetadata fetches metadata about specified RDS instance.
func fetchRDSInstanceMetadata(ctx context.Context, rdsClient rdsiface.RDSAPI, instanceID string) (*types.AWS, error) {
rdsInstance, err := describeRDSInstance(ctx, rdsClient, instanceID)
if err != nil {
return nil, trace.Wrap(err)
}
return services.MetadataFromRDSInstance(rdsInstance)
}
// describeRDSInstance returns AWS RDS instance for the specified ID.
func describeRDSInstance(ctx context.Context, rdsClient rdsiface.RDSAPI, instanceID string) (*rds.DBInstance, error) {
out, err := rdsClient.DescribeDBInstancesWithContext(ctx, &rds.DescribeDBInstancesInput{
DBInstanceIdentifier: aws.String(instanceID),
})
if err != nil {
return nil, common.ConvertError(err)
}
if len(out.DBInstances) != 1 {
return nil, trace.BadParameter("expected 1 RDS instance for %v, got %+v", instanceID, out.DBInstances)
}
return out.DBInstances[0], nil
}
// fetchRDSClusterMetadata fetches metadata about specified Aurora cluster.
func fetchRDSClusterMetadata(ctx context.Context, rdsClient rdsiface.RDSAPI, clusterID string) (*types.AWS, error) {
rdsCluster, err := describeRDSCluster(ctx, rdsClient, clusterID)
if err != nil {
return nil, trace.Wrap(err)
}
return services.MetadataFromRDSCluster(rdsCluster)
}
// describeRDSCluster returns AWS Aurora cluster for the specified ID.
func describeRDSCluster(ctx context.Context, rdsClient rdsiface.RDSAPI, clusterID string) (*rds.DBCluster, error) {
out, err := rdsClient.DescribeDBClustersWithContext(ctx, &rds.DescribeDBClustersInput{
DBClusterIdentifier: aws.String(clusterID),
})
if err != nil {
return nil, common.ConvertError(err)
}
if len(out.DBClusters) != 1 {
return nil, trace.BadParameter("expected 1 RDS cluster for %v, got %+v", clusterID, out.DBClusters)
}
return out.DBClusters[0], nil
}
// describeRedshiftCluster returns AWS Redshift cluster for the specified ID.
func describeRedshiftCluster(ctx context.Context, redshiftClient redshiftiface.RedshiftAPI, clusterID string) (*redshift.Cluster, error) {
out, err := redshiftClient.DescribeClustersWithContext(ctx, &redshift.DescribeClustersInput{
ClusterIdentifier: aws.String(clusterID),
})
if err != nil {
return nil, common.ConvertError(err)
}
if len(out.Clusters) != 1 {
return nil, trace.BadParameter("expected 1 Redshift cluster for %v, got %+v", clusterID, out.Clusters)
}
return out.Clusters[0], nil
}
// describeElastiCacheCluster returns AWS ElastiCache Redis cluster for the
// specified ID.
func describeElastiCacheCluster(ctx context.Context, elastiCacheClient elasticacheiface.ElastiCacheAPI, replicationGroupID string) (*elasticache.ReplicationGroup, error) {
out, err := elastiCacheClient.DescribeReplicationGroupsWithContext(ctx, &elasticache.DescribeReplicationGroupsInput{
ReplicationGroupId: aws.String(replicationGroupID),
})
if err != nil {
return nil, common.ConvertError(err)
}
if len(out.ReplicationGroups) != 1 {
return nil, trace.BadParameter("expected 1 ElastiCache cluster for %v, got %+v", replicationGroupID, out.ReplicationGroups)
}
return out.ReplicationGroups[0], nil
}
// describeMemoryDBCluster returns AWS MemoryDB cluster for the specified ID.
func describeMemoryDBCluster(ctx context.Context, client memorydbiface.MemoryDBAPI, clusterName string) (*memorydb.Cluster, error) {
out, err := client.DescribeClustersWithContext(ctx, &memorydb.DescribeClustersInput{
ClusterName: aws.String(clusterName),
})
if err != nil {
return nil, common.ConvertError(err)
}
if len(out.Clusters) != 1 {
return nil, trace.BadParameter("expected 1 MemoryDB cluster for %v, got %+v", clusterName, out.Clusters)
}
return out.Clusters[0], nil
}
// fetchRDSProxyMetadata fetches metadata about specified RDS Proxy name.
func fetchRDSProxyMetadata(ctx context.Context, rdsClient rdsiface.RDSAPI, proxyName string) (*types.AWS, error) {
rdsProxy, err := describeRDSProxy(ctx, rdsClient, proxyName)
if err != nil {
return nil, trace.Wrap(err)
}
return services.MetadataFromRDSProxy(rdsProxy)
}
// describeRDSProxy returns AWS RDS Proxy for the specified RDS Proxy name.
func describeRDSProxy(ctx context.Context, rdsClient rdsiface.RDSAPI, proxyName string) (*rds.DBProxy, error) {
out, err := rdsClient.DescribeDBProxiesWithContext(ctx, &rds.DescribeDBProxiesInput{
DBProxyName: aws.String(proxyName),
})
if err != nil {
return nil, common.ConvertError(err)
}
if len(out.DBProxies) != 1 {
return nil, trace.BadParameter("expected 1 RDS Proxy for %v, got %s", proxyName, out.DBProxies)
}
return out.DBProxies[0], nil
}
// fetchRDSProxyCustomEndpointMetadata fetches metadata about specified RDS
// proxy custom endpoint.
func fetchRDSProxyCustomEndpointMetadata(ctx context.Context, rdsClient rdsiface.RDSAPI, proxyEndpointName, uri string) (*types.AWS, error) {
rdsProxyEndpoint, err := describeRDSProxyCustomEndpoint(ctx, rdsClient, proxyEndpointName, uri)
if err != nil {
return nil, trace.Wrap(err)
}
rdsProxy, err := describeRDSProxy(ctx, rdsClient, aws.StringValue(rdsProxyEndpoint.DBProxyName))
if err != nil {
return nil, trace.Wrap(err)
}
return services.MetadataFromRDSProxyCustomEndpoint(rdsProxy, rdsProxyEndpoint)
}
// describeRDSProxyCustomEndpoint returns AWS RDS Proxy endpoint for the
// specified RDS Proxy custom endpoint.
func describeRDSProxyCustomEndpoint(ctx context.Context, rdsClient rdsiface.RDSAPI, proxyEndpointName, uri string) (*rds.DBProxyEndpoint, error) {
out, err := rdsClient.DescribeDBProxyEndpointsWithContext(ctx, &rds.DescribeDBProxyEndpointsInput{
DBProxyEndpointName: aws.String(proxyEndpointName),
})
if err != nil {
return nil, common.ConvertError(err)
}
for _, customEndpoint := range out.DBProxyEndpoints {
// Double check if it has the same URI in case multiple custom
// endpoints have the same name.
if strings.Contains(uri, aws.StringValue(customEndpoint.Endpoint)) {
return customEndpoint, nil
}
}
return nil, trace.BadParameter("could not find RDS Proxy custom endpoint %v with URI %v, got %s", proxyEndpointName, uri, out.DBProxyEndpoints)
}
func fetchRedshiftServerlessWorkgroupMetadata(ctx context.Context, client redshiftserverlessiface.RedshiftServerlessAPI, workgroupName string) (*types.AWS, error) {
workgroup, err := describeRedshiftServerlessWorkgroup(ctx, client, workgroupName)
if err != nil {
return nil, trace.Wrap(err)
}
return services.MetadataFromRedshiftServerlessWorkgroup(workgroup)
}
func fetchRedshiftServerlessVPCEndpointMetadata(ctx context.Context, client redshiftserverlessiface.RedshiftServerlessAPI, endpointName string) (*types.AWS, error) {
endpoint, err := describeRedshiftServerlessVCPEndpoint(ctx, client, endpointName)
if err != nil {
return nil, trace.Wrap(err)
}
workgroup, err := describeRedshiftServerlessWorkgroup(ctx, client, aws.StringValue(endpoint.WorkgroupName))
if err != nil {
return nil, trace.Wrap(err)
}
return services.MetadataFromRedshiftServerlessVPCEndpoint(endpoint, workgroup)
}
func describeRedshiftServerlessWorkgroup(ctx context.Context, client redshiftserverlessiface.RedshiftServerlessAPI, workgroupName string) (*redshiftserverless.Workgroup, error) {
input := new(redshiftserverless.GetWorkgroupInput).SetWorkgroupName(workgroupName)
output, err := client.GetWorkgroupWithContext(ctx, input)
if err != nil {
return nil, common.ConvertError(err)
}
return output.Workgroup, nil
}
func describeRedshiftServerlessVCPEndpoint(ctx context.Context, client redshiftserverlessiface.RedshiftServerlessAPI, endpointName string) (*redshiftserverless.EndpointAccess, error) {
input := new(redshiftserverless.GetEndpointAccessInput).SetEndpointName(endpointName)
output, err := client.GetEndpointAccessWithContext(ctx, input)
if err != nil {
return nil, common.ConvertError(err)
}
return output.Endpoint, nil
}