diff --git a/.changelog/32659.txt b/.changelog/32659.txt new file mode 100644 index 000000000000..bc6faaf19156 --- /dev/null +++ b/.changelog/32659.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticsearch_domain: Omit `ebs_options.throughput` and `ebs_options.iops` for unsupported volume types +``` diff --git a/internal/service/elasticsearch/domain.go b/internal/service/elasticsearch/domain.go index 3bd96669e00d..fc9e5841ff77 100644 --- a/internal/service/elasticsearch/domain.go +++ b/internal/service/elasticsearch/domain.go @@ -1249,3 +1249,31 @@ func advancedOptionsIgnoreDefault(o map[string]interface{}, n map[string]interfa return n } + +// EBSVolumeTypePermitsIopsInput returns true if the volume type supports the Iops input +// +// This check prevents a ValidationException when updating EBS volume types from a value +// that supports IOPS (ex. gp3) to one that doesn't (ex. gp2). +func EBSVolumeTypePermitsIopsInput(volumeType string) bool { + permittedTypes := []string{elasticsearch.VolumeTypeGp3, elasticsearch.VolumeTypeIo1} + for _, t := range permittedTypes { + if volumeType == t { + return true + } + } + return false +} + +// EBSVolumeTypePermitsIopsInput returns true if the volume type supports the Throughput input +// +// This check prevents a ValidationException when updating EBS volume types from a value +// that supports Throughput (ex. gp3) to one that doesn't (ex. gp2). +func EBSVolumeTypePermitsThroughputInput(volumeType string) bool { + permittedTypes := []string{elasticsearch.VolumeTypeGp3} + for _, t := range permittedTypes { + if volumeType == t { + return true + } + } + return false +} diff --git a/internal/service/elasticsearch/domain_test.go b/internal/service/elasticsearch/domain_test.go index 872494da9968..4f3e0170690c 100644 --- a/internal/service/elasticsearch/domain_test.go +++ b/internal/service/elasticsearch/domain_test.go @@ -22,6 +22,58 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) +func TestEBSVolumeTypePermitsIopsInput(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + volumeType string + want bool + }{ + {"empty", "", false}, + {"gp2", elasticsearch.VolumeTypeGp2, false}, + {"gp3", elasticsearch.VolumeTypeGp3, true}, + {"io1", elasticsearch.VolumeTypeIo1, true}, + {"standard", elasticsearch.VolumeTypeStandard, false}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + if got := tfelasticsearch.EBSVolumeTypePermitsIopsInput(testCase.volumeType); got != testCase.want { + t.Errorf("EBSVolumeTypePermitsIopsInput() = %v, want %v", got, testCase.want) + } + }) + } +} + +func TestEBSVolumeTypePermitsThroughputInput(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + volumeType string + want bool + }{ + {"empty", "", false}, + {"gp2", elasticsearch.VolumeTypeGp2, false}, + {"gp3", elasticsearch.VolumeTypeGp3, true}, + {"io1", elasticsearch.VolumeTypeIo1, false}, + {"standard", elasticsearch.VolumeTypeStandard, false}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + if got := tfelasticsearch.EBSVolumeTypePermitsThroughputInput(testCase.volumeType); got != testCase.want { + t.Errorf("EBSVolumeTypePermitsThroughputInput() = %v, want %v", got, testCase.want) + } + }) + } +} + func TestAccElasticsearchDomain_basic(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -1429,6 +1481,55 @@ func TestAccElasticsearchDomain_VolumeType_update(t *testing.T) { }}) } +// Verifies that EBS volume_type can be changed from gp3 to a type which does not +// support the throughput and iops input values (ex. gp2) +// +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/32613 +func TestAccElasticsearchDomain_VolumeType_gp3ToGP2(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var input elasticsearch.ElasticsearchDomainStatus + rName := testAccRandomDomainName() + resourceName := "aws_elasticsearch_domain.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckIAMServiceLinkedRole(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elasticsearch.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckDomainDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDomainConfig_clusterEBSVolumeGP3DefaultIopsThroughput(rName, 10), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainExists(ctx, resourceName, &input), + resource.TestCheckResourceAttr(resourceName, "ebs_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ebs_options.0.ebs_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "ebs_options.0.volume_size", "10"), + resource.TestCheckResourceAttr(resourceName, "ebs_options.0.volume_type", "gp3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateId: rName, + ImportStateVerify: true, + }, + { + Config: testAccDomainConfig_clusterEBSVolumeGP2(rName, 10), + Check: resource.ComposeTestCheckFunc( + testAccCheckDomainExists(ctx, resourceName, &input), + resource.TestCheckResourceAttr(resourceName, "ebs_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "ebs_options.0.ebs_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "ebs_options.0.volume_size", "10"), + resource.TestCheckResourceAttr(resourceName, "ebs_options.0.volume_type", "gp2"), + ), + }, + }}) +} + // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/13867 func TestAccElasticsearchDomain_VolumeType_missing(t *testing.T) { if testing.Short() { @@ -2117,6 +2218,44 @@ resource "aws_elasticsearch_domain" "test" { `, rName, volumeSize, volumeThroughput, volumeIops) } +func testAccDomainConfig_clusterEBSVolumeGP3DefaultIopsThroughput(rName string, volumeSize int) string { + return fmt.Sprintf(` +resource "aws_elasticsearch_domain" "test" { + domain_name = %[1]q + elasticsearch_version = "7.10" + + ebs_options { + ebs_enabled = true + volume_size = %[2]d + volume_type = "gp3" + } + + cluster_config { + instance_type = "t3.small.elasticsearch" + } +} +`, rName, volumeSize) +} + +func testAccDomainConfig_clusterEBSVolumeGP2(rName string, volumeSize int) string { + return fmt.Sprintf(` +resource "aws_elasticsearch_domain" "test" { + domain_name = %[1]q + elasticsearch_version = "7.10" + + ebs_options { + ebs_enabled = true + volume_size = %[2]d + volume_type = "gp2" + } + + cluster_config { + instance_type = "t3.small.elasticsearch" + } +} +`, rName, volumeSize) +} + func testAccDomainConfig_clusterUpdateVersion(rName, version string) string { return fmt.Sprintf(` resource "aws_elasticsearch_domain" "test" { @@ -2915,16 +3054,16 @@ func testAccDomainConfig_logPublishingOptions(rName, logType string) string { master_user_password = "Barbarbarbar1!" } } - + domain_endpoint_options { enforce_https = true tls_security_policy = "Policy-Min-TLS-1-2-2019-07" } - + encrypt_at_rest { enabled = true } - + node_to_node_encryption { enabled = true }` diff --git a/internal/service/elasticsearch/flex.go b/internal/service/elasticsearch/flex.go index 85038af5b042..209a2b16f07f 100644 --- a/internal/service/elasticsearch/flex.go +++ b/internal/service/elasticsearch/flex.go @@ -79,17 +79,19 @@ func expandEBSOptions(m map[string]interface{}) *elasticsearch.EBSOptions { options.EBSEnabled = aws.Bool(ebsEnabled.(bool)) if ebsEnabled.(bool) { - if v, ok := m["iops"]; ok && v.(int) > 0 { - options.Iops = aws.Int64(int64(v.(int))) - } - if v, ok := m["throughput"]; ok && v.(int) > 0 { - options.Throughput = aws.Int64(int64(v.(int))) - } if v, ok := m["volume_size"]; ok && v.(int) > 0 { options.VolumeSize = aws.Int64(int64(v.(int))) } + var volumeType string if v, ok := m["volume_type"]; ok && v.(string) != "" { - options.VolumeType = aws.String(v.(string)) + volumeType = v.(string) + options.VolumeType = aws.String(volumeType) + } + if v, ok := m["iops"]; ok && v.(int) > 0 && EBSVolumeTypePermitsIopsInput(volumeType) { + options.Iops = aws.Int64(int64(v.(int))) + } + if v, ok := m["throughput"]; ok && v.(int) > 0 && EBSVolumeTypePermitsThroughputInput(volumeType) { + options.Throughput = aws.Int64(int64(v.(int))) } } }