diff --git a/examples/terraform-aws-rds-example/main.tf b/examples/terraform-aws-rds-example/main.tf index 0ffdccc70..35d7cb228 100644 --- a/examples/terraform-aws-rds-example/main.tf +++ b/examples/terraform-aws-rds-example/main.tf @@ -105,7 +105,7 @@ resource "aws_db_instance" "example" { name = var.database_name username = var.username password = var.password - instance_class = "db.t2.micro" + instance_class = var.instance_class allocated_storage = var.allocated_storage skip_final_snapshot = true license_model = var.license_model diff --git a/examples/terraform-aws-rds-example/variables.tf b/examples/terraform-aws-rds-example/variables.tf index 6c15af6a3..94bf7e62e 100644 --- a/examples/terraform-aws-rds-example/variables.tf +++ b/examples/terraform-aws-rds-example/variables.tf @@ -80,3 +80,9 @@ variable "license_model" { default = "general-public-license" } +variable "instance_class" { + description = "Instance class to be used to run the database" + type = string + default = "db.t2.micro" +} + diff --git a/examples/terraform-remote-exec-example/main.tf b/examples/terraform-remote-exec-example/main.tf index 1fb56bb01..8d221844e 100644 --- a/examples/terraform-remote-exec-example/main.tf +++ b/examples/terraform-remote-exec-example/main.tf @@ -25,7 +25,7 @@ provider "aws" { resource "aws_instance" "example_public" { ami = data.aws_ami.ubuntu.id - instance_type = "t2.micro" + instance_type = var.instance_type vpc_security_group_ids = [aws_security_group.example.id] key_name = var.key_pair_name diff --git a/examples/terraform-remote-exec-example/variables.tf b/examples/terraform-remote-exec-example/variables.tf index 818a6bc7f..17c7c9b7e 100644 --- a/examples/terraform-remote-exec-example/variables.tf +++ b/examples/terraform-remote-exec-example/variables.tf @@ -45,3 +45,9 @@ variable "ssh_user" { default = "ubuntu" } +variable "instance_type" { + description = "Instance type to use for EC2 Instance" + type = string + default = "t2.micro" +} + diff --git a/modules/aws/errors.go b/modules/aws/errors.go index 7f926dbc4..16c127f28 100644 --- a/modules/aws/errors.go +++ b/modules/aws/errors.go @@ -113,3 +113,19 @@ func (err NoInstanceTypeError) Error() string { err.Azs, ) } + +// NoRdsInstanceTypeError is returned when none of the given instance types are avaiable for the region, database engine, and database engine combination given +type NoRdsInstanceTypeError struct { + InstanceTypeOptions []string + DatabaseEngine string + DatabaseEngineVersion string +} + +func (err NoRdsInstanceTypeError) Error() string { + return fmt.Sprintf( + "None of the given RDS instance types (%v) is available in this region for database engine (%v) of version (%v).", + err.InstanceTypeOptions, + err.DatabaseEngine, + err.DatabaseEngineVersion, + ) +} diff --git a/modules/aws/rds.go b/modules/aws/rds.go index e0aad284c..25afcf56f 100644 --- a/modules/aws/rds.go +++ b/modules/aws/rds.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/service/rds" _ "github.com/go-sql-driver/mysql" "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" ) // GetAddressOfRdsInstance gets the address of the given RDS Instance in the given region. @@ -217,6 +218,65 @@ func NewRdsClientE(t testing.TestingT, region string) (*rds.RDS, error) { return rds.New(sess), nil } +// GetRecommendedRdsInstanceType takes in a list of RDS instance types (e.g., "db.t2.micro", "db.t3.micro") and returns the +// first instance type in the list that is available in the given region and for the given database engine type. +// If none of the instances provided are avaiable for your combination of region and database engine, this function will exit with an error. +func GetRecommendedRdsInstanceType(t testing.TestingT, region string, engine string, engineVersion string, instanceTypeOptions []string) string { + out, err := GetRecommendedRdsInstanceTypeE(t, region, engine, engineVersion, instanceTypeOptions) + require.NoError(t, err) + return out +} + +// GetRecommendedRdsInstanceTypeE takes in a list of RDS instance types (e.g., "db.t2.micro", "db.t3.micro") and returns the +// first instance type in the list that is available in the given region and for the given database engine type. +// If none of the instances provided are avaiable for your combination of region and database engine, this function will return an error. +func GetRecommendedRdsInstanceTypeE(t testing.TestingT, region string, engine string, engineVersion string, instanceTypeOptions []string) (string, error) { + client, err := NewRdsClientE(t, region) + if err != nil { + return "", err + } + return GetRecommendedRdsInstanceTypeWithClientE(t, client, engine, engineVersion, instanceTypeOptions) +} + +// GetRecommendedRdsInstanceTypeWithClientE takes in a list of RDS instance types (e.g., "db.t2.micro", "db.t3.micro") and returns the +// first instance type in the list that is available in the given region and for the given database engine type. +// If none of the instances provided are avaiable for your combination of region and database engine, this function will return an error. +// This function expects an authenticated RDS client from the AWS SDK Go library. +func GetRecommendedRdsInstanceTypeWithClientE(t testing.TestingT, rdsClient *rds.RDS, engine string, engineVersion string, instanceTypeOptions []string) (string, error) { + for _, instanceTypeOption := range instanceTypeOptions { + instanceTypeExists, err := instanceTypeExistsForEngineAndRegionE(rdsClient, engine, engineVersion, instanceTypeOption) + if err != nil { + return "", err + } + + if instanceTypeExists { + return instanceTypeOption, nil + } + } + return "", NoRdsInstanceTypeError{InstanceTypeOptions: instanceTypeOptions, DatabaseEngine: engine, DatabaseEngineVersion: engineVersion} +} + +// instanceTypeExistsForEngineAndRegionE returns a boolean that represents whether the provided instance type (e.g. db.t2.micro) exists for the given region and db engine type +// This function will return an error if the RDS AWS SDK call fails. +func instanceTypeExistsForEngineAndRegionE(client *rds.RDS, engine string, engineVersion string, instanceType string) (bool, error) { + input := rds.DescribeOrderableDBInstanceOptionsInput{ + Engine: aws.String(engine), + EngineVersion: aws.String(engineVersion), + DBInstanceClass: aws.String(instanceType), + } + + out, err := client.DescribeOrderableDBInstanceOptions(&input) + if err != nil { + return false, err + } + + if len(out.OrderableDBInstanceOptions) > 0 { + return true, nil + } + + return false, nil +} + // ParameterForDbInstanceNotFound is an error that occurs when the parameter group specified is not found for the DB instance type ParameterForDbInstanceNotFound struct { ParameterName string diff --git a/modules/aws/rds_test.go b/modules/aws/rds_test.go new file mode 100644 index 000000000..fdf60c565 --- /dev/null +++ b/modules/aws/rds_test.go @@ -0,0 +1,147 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetRecommendedRdsInstanceTypeHappyPath(t *testing.T) { + type TestingScenerios struct { + name string + region string + databaseEngine string + databaseEngineVersion string + instanceTypes []string + expected string + } + + testingScenerios := []TestingScenerios{ + { + name: "US region, mysql, first offering available", + region: "us-east-2", + databaseEngine: "mysql", + databaseEngineVersion: "8.0.21", + instanceTypes: []string{"db.t2.micro", "db.t3.micro"}, + expected: "db.t2.micro", + }, + { + name: "EU region, postgres, 2nd offering available based on region", + region: "eu-north-1", + databaseEngine: "postgres", + databaseEngineVersion: "13.1", + instanceTypes: []string{"db.t2.micro", "db.m5.large"}, + expected: "db.m5.large", + }, + { + name: "US region, oracle-ee, 2nd offering available based on db type", + region: "us-west-2", + databaseEngine: "oracle-ee", + databaseEngineVersion: "19.0.0.0.ru-2021-01.rur-2021-01.r1", + instanceTypes: []string{"db.m5d.xlarge", "db.m5.large"}, + expected: "db.m5.large", + }, + { + name: "US region, oracle-ee, 2nd offering available based on db engine version", + region: "us-west-2", + databaseEngine: "oracle-ee", + databaseEngineVersion: "19.0.0.0.ru-2021-01.rur-2021-01.r1", + instanceTypes: []string{"db.t3.micro", "db.t3.small"}, + expected: "db.t3.small", + }, + } + + for _, scenerio := range testingScenerios { + scenerio := scenerio + + t.Run(scenerio.name, func(t *testing.T) { + t.Parallel() + + actual, err := GetRecommendedRdsInstanceTypeE(t, scenerio.region, scenerio.databaseEngine, scenerio.databaseEngineVersion, scenerio.instanceTypes) + assert.NoError(t, err) + assert.Equal(t, scenerio.expected, actual) + }) + } +} + +func TestGetRecommendedRdsInstanceTypeErrors(t *testing.T) { + type TestingScenerios struct { + name string + region string + databaseEngine string + databaseEngineVersion string + instanceTypes []string + } + + testingScenerios := []TestingScenerios{ + { + name: "All empty", + region: "", + databaseEngine: "", + databaseEngineVersion: "", + instanceTypes: nil, + }, + { + name: "No engine, version, or instance type", + region: "us-east-2", + databaseEngine: "", + databaseEngineVersion: "", + instanceTypes: nil, + }, + { + name: "No instance types or version", + region: "us-east-2", + databaseEngine: "mysql", + databaseEngineVersion: "", + instanceTypes: nil, + }, + { + name: "No engine version", + region: "us-east-2", + databaseEngine: "mysql", + databaseEngineVersion: "", + instanceTypes: []string{"db.t3.small"}, + }, + { + name: "Invalid instance types", + region: "us-east-2", + databaseEngine: "mysql", + databaseEngineVersion: "8.0.21", + instanceTypes: []string{"garbage"}, + }, + { + name: "Region has no instance type available", + region: "eu-north-1", + databaseEngine: "mysql", + databaseEngineVersion: "8.0.21", + instanceTypes: []string{"db.t2.micro"}, + }, + { + name: "No instance type available for engine", + region: "us-east-1", + databaseEngine: "oracle-ee", + databaseEngineVersion: "19.0.0.0.ru-2021-01.rur-2021-01.r1", + instanceTypes: []string{"db.r5d.large"}, + }, + { + name: "No instance type available for engine version", + region: "us-east-1", + databaseEngine: "oracle-ee", + databaseEngineVersion: "19.0.0.0.ru-2021-01.rur-2021-01.r1", + instanceTypes: []string{"db.t3.micro"}, + }, + } + + for _, scenerio := range testingScenerios { + scenerio := scenerio + + t.Run(scenerio.name, func(t *testing.T) { + t.Parallel() + + _, err := GetRecommendedRdsInstanceTypeE(t, scenerio.region, scenerio.databaseEngine, scenerio.databaseEngineVersion, scenerio.instanceTypes) + fmt.Println(err) + assert.EqualError(t, err, NoRdsInstanceTypeError{InstanceTypeOptions: scenerio.instanceTypes, DatabaseEngine: scenerio.databaseEngine, DatabaseEngineVersion: scenerio.databaseEngineVersion}.Error()) + }) + } +} diff --git a/test/terraform_aws_rds_example_test.go b/test/terraform_aws_rds_example_test.go index bd6f1a10a..4cb422a4d 100644 --- a/test/terraform_aws_rds_example_test.go +++ b/test/terraform_aws_rds_example_test.go @@ -24,6 +24,7 @@ func TestTerraformAwsRdsExample(t *testing.T) { password := "password" // Pick a random AWS region to test in. This helps ensure your code works in all regions. awsRegion := aws.GetRandomStableRegion(t, nil, nil) + instanceType := aws.GetRecommendedRdsInstanceType(t, awsRegion, "mysql", "5.7.21", []string{"db.t2.micro", "db.t3.micro"}) // Construct the terraform options with default retryable errors to handle the most common retryable errors in // terraform testing. @@ -38,6 +39,7 @@ func TestTerraformAwsRdsExample(t *testing.T) { "engine_name": "mysql", "major_engine_version": "5.7", "family": "mysql5.7", + "instance_class": instanceType, "username": username, "password": password, "allocated_storage": 5, diff --git a/test/terraform_remote_exec_example_test.go b/test/terraform_remote_exec_example_test.go index bb985e34b..91f8cfa3c 100644 --- a/test/terraform_remote_exec_example_test.go +++ b/test/terraform_remote_exec_example_test.go @@ -54,6 +54,9 @@ func TestTerraformRemoteExecExample(t *testing.T) { // Pick a random AWS region to test in. This helps ensure your code works in all regions. awsRegion := aws.GetRandomStableRegion(t, nil, nil) + // Some AWS regions are missing certain instance types, so pick an available type based on the region we picked + instanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{"t2.micro", "t3.micro"}) + // Create an EC2 KeyPair that we can use for SSH access keyPairName := fmt.Sprintf("terratest-remote-exec-example-%s", uniqueID) keyPair := aws.CreateAndImportEC2KeyPair(t, awsRegion, keyPairName) @@ -72,6 +75,7 @@ func TestTerraformRemoteExecExample(t *testing.T) { Vars: map[string]interface{}{ "aws_region": awsRegion, "instance_name": instanceName, + "instance_type": instanceType, "key_pair_name": keyPairName, },