Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

skip databases that are not available during auto discovery #10699

Merged
merged 9 commits into from
Mar 4, 2022
52 changes: 52 additions & 0 deletions lib/services/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,58 @@ func IsRDSClusterSupported(cluster *rds.DBCluster) bool {
return true
}

// IsRDSInstanceAvailable checks if the RDS instance is available.
func IsRDSInstanceAvailable(instance *rds.DBInstance) bool {
// For a full list of status values, see:
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/accessing-monitoring.html
switch aws.StringValue(instance.DBInstanceStatus) {

// Statues marked as "Not billed" in the above guide.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Statues marked as "Not billed" in the above guide.
// Statuses marked as "Not billed" in the above guide.

case "creating", "deleting", "failed",
"inaccessible-encryption-credentials", "incompatible-network",
"incompatible-restore":
return false

// Statuses marked as "Billed for storage" in the above guide.
case "inaccessible-encryption-credentials-recoverable", "starting",
"stopped", "stopping":
return false
}

return true
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
}

// IsRDSClusterAvailable checks if the RDS cluster is available.
func IsRDSClusterAvailable(cluster *rds.DBCluster) bool {
// For a full list of status values, see:
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/accessing-monitoring.html
switch aws.StringValue(cluster.Status) {

// Statues marked as "Not billed" in the above guide.
case "cloning-failed", "creating", "deleting",
"inaccessible-encryption-credentials", "migration-failed":
return false

// Statuses marked as "Billed for storage" in the above guide.
case "starting", "stopped", "stopping":
return false
}

return true
}

// IsRedshiftClusterAvailable checks if the Redshift cluster is available.
func IsRedshiftClusterAvailable(cluster *redshift.Cluster) bool {
// For a full list of status values, see:
// https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#rs-mgmt-cluster-status
switch aws.StringValue(cluster.ClusterStatus) {
case "creating", "deleting", "hardware-failure", "paused":
return false
}

return !strings.HasSuffix(aws.StringValue(cluster.ClusterAvailabilityStatus), "incompatible-")
}

// auroraMySQLVersion extracts aurora mysql version from engine version
func auroraMySQLVersion(cluster *rds.DBCluster) string {
// version guide: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Updates.Versions.html
Expand Down
14 changes: 14 additions & 0 deletions lib/srv/db/cloud/watchers/rds.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ func (f *rdsDBInstancesFetcher) getRDSDatabases(ctx context.Context) (types.Data
}
databases := make(types.Databases, 0, len(instances))
for _, instance := range instances {
if !services.IsRDSInstanceAvailable(instance) {
f.log.Debugf("The current status of RDS instance %q is %q. Skipping.",
aws.StringValue(instance.DBInstanceIdentifier),
aws.StringValue(instance.DBInstanceStatus))
continue
}

database, err := services.NewDatabaseFromRDSInstance(instance)
if err != nil {
f.log.Warnf("Could not convert RDS instance %q to database resource: %v.",
Expand Down Expand Up @@ -195,6 +202,13 @@ func (f *rdsAuroraClustersFetcher) getAuroraDatabases(ctx context.Context) (type
continue
}

if !services.IsRDSClusterAvailable(cluster) {
f.log.Debugf("The current status of Aurora cluster %q is %q. Skipping.",
aws.StringValue(cluster.DBClusterIdentifier),
aws.StringValue(cluster.Status))
continue
}

// Add a database from primary endpoint
database, err := services.NewDatabaseFromRDSCluster(cluster)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions lib/srv/db/cloud/watchers/redshift.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ func (f *redshiftFetcher) Get(ctx context.Context) (types.Databases, error) {

var databases types.Databases
for _, cluster := range clusters {
if !services.IsRedshiftClusterAvailable(cluster) {
f.log.Debugf("The current status of Redshift cluster %q is %q. Skipping.",
aws.StringValue(cluster.ClusterIdentifier),
aws.StringValue(cluster.ClusterStatus))
continue
}

database, err := services.NewDatabaseFromRedshiftCluster(cluster)
if err != nil {
f.log.Infof("Could not convert Redshift cluster %q to database resource: %v.",
Expand Down
98 changes: 88 additions & 10 deletions lib/srv/db/cloud/watchers/watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@ func TestWatcher(t *testing.T) {
rdsInstance2, _ := makeRDSInstance(t, "instance-2", "us-east-2", map[string]string{"env": "prod"})
rdsInstance3, _ := makeRDSInstance(t, "instance-3", "us-east-1", map[string]string{"env": "dev"})
rdsInstance4, rdsDatabase4 := makeRDSInstance(t, "instance-4", "us-west-1", nil)
rdsInstanceUnavailable, _ := makeRDSInstance(t, "instance-5", "us-west-1", nil, rdsInstanceStatus("stopped"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a test that explicitly verifies the behavior against unrecognized status, like "xxx-this-status-does-not-exist"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

=== RUN   TestWatcher/RDS_unavailable_databases_are_skipped
WARN[0000] Unknown status type: "status-does-not-exist". Assuming RDS instance "instance-5" is available. 
WARN[0000] Unknown status type: "status-does-not-exist". Assuming Aurora cluster "cluster-5" is available. 
=== RUN   TestWatcher/skip_access_denied_errors
=== RUN   TestWatcher/Redshift_labels_matching
=== RUN   TestWatcher/Redshift_unavailable_databases_are_skipped
WARN[0000] Unknown status type: "status-does-not-exist". Assuming Redshift cluster "test" is available. 


auroraCluster1, auroraDatabase1 := makeRDSCluster(t, "cluster-1", "us-east-1", services.RDSEngineModeProvisioned, map[string]string{"env": "prod"})
auroraCluster1, auroraDatabase1 := makeRDSCluster(t, "cluster-1", "us-east-1", map[string]string{"env": "prod"})
auroraCluster2, auroraDatabases2 := makeRDSClusterWithExtraEndpoints(t, "cluster-2", "us-east-2", map[string]string{"env": "dev"})
auroraCluster3, _ := makeRDSCluster(t, "cluster-3", "us-east-2", services.RDSEngineModeProvisioned, map[string]string{"env": "prod"})
auroraClusterUnsupported, _ := makeRDSCluster(t, "serverless", "us-east-1", services.RDSEngineModeServerless, map[string]string{"env": "prod"})
auroraCluster3, _ := makeRDSCluster(t, "cluster-3", "us-east-2", map[string]string{"env": "prod"})
auroraClusterUnsupported, _ := makeRDSCluster(t, "serverless", "us-east-1", nil, rdsClusterEngineMode("serverless"))
auroraClusterUnavailable, _ := makeRDSCluster(t, "serverless", "us-east-1", nil, rdsClusterStatus("creating"))

redshiftUse1Prod, redshiftDatabaseUse1Prod := makeRedshiftCluster(t, "us-east-1", "prod")
redshiftUse1Dev, _ := makeRedshiftCluster(t, "us-east-1", "dev")
redshiftUse1Unavailable, _ := makeRedshiftCluster(t, "us-east-1", "qa", redshiftClusterStatus("paused"))

tests := []struct {
name string
Expand All @@ -60,7 +63,7 @@ func TestWatcher(t *testing.T) {
expectedDatabases types.Databases
}{
{
name: "rds labels matching",
name: "RDS labels matching",
awsMatchers: []services.AWSMatcher{
{
Types: []string{services.AWSMatcherRDS},
Expand Down Expand Up @@ -88,7 +91,7 @@ func TestWatcher(t *testing.T) {
expectedDatabases: append(types.Databases{rdsDatabase1, auroraDatabase1}, auroraDatabases2...),
},
{
name: "rds aurora unsupported",
name: "RDS unsupported databases are skipped",
awsMatchers: []services.AWSMatcher{{
Types: []string{services.AWSMatcherRDS},
Regions: []string{"us-east-1"},
Expand All @@ -103,6 +106,21 @@ func TestWatcher(t *testing.T) {
},
expectedDatabases: types.Databases{auroraDatabase1},
},
{
name: "RDS unavailable databases are skipped",
awsMatchers: []services.AWSMatcher{{
Types: []string{services.AWSMatcherRDS},
Regions: []string{"us-east-1"},
Tags: types.Labels{"*": []string{"*"}},
}},
clients: &common.TestCloudClients{
RDS: &cloud.RDSMock{
DBInstances: []*rds.DBInstance{rdsInstance1, rdsInstanceUnavailable},
DBClusters: []*rds.DBCluster{auroraCluster1, auroraClusterUnavailable},
},
},
expectedDatabases: types.Databases{rdsDatabase1, auroraDatabase1},
},
{
name: "skip access denied errors",
awsMatchers: []services.AWSMatcher{{
Expand All @@ -126,7 +144,7 @@ func TestWatcher(t *testing.T) {
expectedDatabases: types.Databases{rdsDatabase4, auroraDatabase1},
},
{
name: "redshift",
name: "Redshift labels matching",
awsMatchers: []services.AWSMatcher{
{
Types: []string{services.AWSMatcherRedshift},
Expand All @@ -141,6 +159,22 @@ func TestWatcher(t *testing.T) {
},
expectedDatabases: types.Databases{redshiftDatabaseUse1Prod},
},
{
name: "Redshift unavailable databases are skipped",
awsMatchers: []services.AWSMatcher{
{
Types: []string{services.AWSMatcherRedshift},
Regions: []string{"us-east-1"},
Tags: types.Labels{"*": []string{"*"}},
},
},
clients: &common.TestCloudClients{
Redshift: &cloud.RedshiftMock{
Clusters: []*redshift.Cluster{redshiftUse1Prod, redshiftUse1Unavailable},
},
},
expectedDatabases: types.Databases{redshiftDatabaseUse1Prod},
},
{
name: "matcher with multiple types",
awsMatchers: []services.AWSMatcher{
Expand Down Expand Up @@ -178,43 +212,54 @@ func TestWatcher(t *testing.T) {
}
}

func makeRDSInstance(t *testing.T, name, region string, labels map[string]string) (*rds.DBInstance, types.Database) {
func makeRDSInstance(t *testing.T, name, region string, labels map[string]string, opts ...func(*rds.DBInstance)) (*rds.DBInstance, types.Database) {
instance := &rds.DBInstance{
DBInstanceArn: aws.String(fmt.Sprintf("arn:aws:rds:%v:1234567890:db:%v", region, name)),
DBInstanceIdentifier: aws.String(name),
DbiResourceId: aws.String(uuid.New().String()),
Engine: aws.String(services.RDSEnginePostgres),
DBInstanceStatus: aws.String("available"),
Endpoint: &rds.Endpoint{
Address: aws.String("localhost"),
Port: aws.Int64(5432),
},
TagList: labelsToTags(labels),
}
for _, opt := range opts {
opt(instance)
}

database, err := services.NewDatabaseFromRDSInstance(instance)
require.NoError(t, err)
return instance, database
}

func makeRDSCluster(t *testing.T, name, region, engineMode string, labels map[string]string) (*rds.DBCluster, types.Database) {
func makeRDSCluster(t *testing.T, name, region string, labels map[string]string, opts ...func(*rds.DBCluster)) (*rds.DBCluster, types.Database) {
cluster := &rds.DBCluster{
DBClusterArn: aws.String(fmt.Sprintf("arn:aws:rds:%v:1234567890:cluster:%v", region, name)),
DBClusterIdentifier: aws.String(name),
DbClusterResourceId: aws.String(uuid.New().String()),
Engine: aws.String(services.RDSEngineAuroraMySQL),
EngineMode: aws.String(engineMode),
EngineMode: aws.String(services.RDSEngineModeProvisioned),
Status: aws.String("available"),
Endpoint: aws.String("localhost"),
Port: aws.Int64(3306),
TagList: labelsToTags(labels),
}
for _, opt := range opts {
opt(cluster)
}

database, err := services.NewDatabaseFromRDSCluster(cluster)
require.NoError(t, err)
return cluster, database
}

func makeRedshiftCluster(t *testing.T, region, env string) (*redshift.Cluster, types.Database) {
func makeRedshiftCluster(t *testing.T, region, env string, opts ...func(*redshift.Cluster)) (*redshift.Cluster, types.Database) {
cluster := &redshift.Cluster{
ClusterIdentifier: aws.String(env),
ClusterNamespaceArn: aws.String(fmt.Sprintf("arn:aws:redshift:%s:1234567890:namespace:%s", region, env)),
ClusterStatus: aws.String("available"),
Endpoint: &redshift.Endpoint{
Address: aws.String("localhost"),
Port: aws.Int64(5439),
Expand All @@ -224,6 +269,10 @@ func makeRedshiftCluster(t *testing.T, region, env string) (*redshift.Cluster, t
Value: aws.String(env),
}},
}
for _, opt := range opts {
opt(cluster)
}

database, err := services.NewDatabaseFromRedshiftCluster(cluster)
require.NoError(t, err)
return cluster, database
Expand All @@ -236,6 +285,7 @@ func makeRDSClusterWithExtraEndpoints(t *testing.T, name, region string, labels
DbClusterResourceId: aws.String(uuid.New().String()),
Engine: aws.String(services.RDSEngineAuroraMySQL),
EngineMode: aws.String(services.RDSEngineModeProvisioned),
Status: aws.String("available"),
Endpoint: aws.String("localhost"),
ReaderEndpoint: aws.String("reader.host"),
Port: aws.Int64(3306),
Expand All @@ -259,6 +309,34 @@ func makeRDSClusterWithExtraEndpoints(t *testing.T, name, region string, labels
return cluster, append(types.Databases{primaryDatabase, readerDatabase}, customDatabases...)
}

// rdsInstanceStatus returns an option function for makeRDSInstance to overwrite status.
func rdsInstanceStatus(status string) func(*rds.DBInstance) {
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
return func(instance *rds.DBInstance) {
instance.DBInstanceStatus = aws.String(status)
}
}

// rdsClusterEngineMode returns an option function for makeRDSCluster to overwrite engine mode.
func rdsClusterEngineMode(mode string) func(*rds.DBCluster) {
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
return func(cluster *rds.DBCluster) {
cluster.EngineMode = aws.String(mode)
}
}

// rdsClusterStatus returns an option function for makeRDSCluster to overwrite status.
func rdsClusterStatus(status string) func(*rds.DBCluster) {
return func(cluster *rds.DBCluster) {
cluster.Status = aws.String(status)
}
}

// redshiftClusterStatus returns an option function for makeRedshiftCluster to overwrite status.
func redshiftClusterStatus(status string) func(*redshift.Cluster) {
return func(cluster *redshift.Cluster) {
cluster.ClusterStatus = aws.String(status)
}
}

func labelsToTags(labels map[string]string) (tags []*rds.Tag) {
for key, val := range labels {
tags = append(tags, &rds.Tag{
Expand Down