diff --git a/.changelog/22304.txt b/.changelog/22304.txt new file mode 100644 index 00000000000..27698ee932f --- /dev/null +++ b/.changelog/22304.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_memorydb_parameter_group +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a104147da6b..133a8c6dcb0 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1384,9 +1384,10 @@ func Provider() *schema.Provider { "aws_media_store_container": mediastore.ResourceContainer(), "aws_media_store_container_policy": mediastore.ResourceContainerPolicy(), - "aws_memorydb_acl": memorydb.ResourceACL(), - "aws_memorydb_subnet_group": memorydb.ResourceSubnetGroup(), - "aws_memorydb_user": memorydb.ResourceUser(), + "aws_memorydb_acl": memorydb.ResourceACL(), + "aws_memorydb_parameter_group": memorydb.ResourceParameterGroup(), + "aws_memorydb_subnet_group": memorydb.ResourceSubnetGroup(), + "aws_memorydb_user": memorydb.ResourceUser(), "aws_mq_broker": mq.ResourceBroker(), "aws_mq_configuration": mq.ResourceConfiguration(), diff --git a/internal/service/memorydb/find.go b/internal/service/memorydb/find.go index 157522a127a..8e78efcdbaf 100644 --- a/internal/service/memorydb/find.go +++ b/internal/service/memorydb/find.go @@ -39,6 +39,35 @@ func FindACLByName(ctx context.Context, conn *memorydb.MemoryDB, name string) (* return output.ACLs[0], nil } +func FindParameterGroupByName(ctx context.Context, conn *memorydb.MemoryDB, name string) (*memorydb.ParameterGroup, error) { + input := memorydb.DescribeParameterGroupsInput{ + ParameterGroupName: aws.String(name), + } + + output, err := conn.DescribeParameterGroupsWithContext(ctx, &input) + + if tfawserr.ErrCodeEquals(err, memorydb.ErrCodeParameterGroupNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.ParameterGroups) == 0 || output.ParameterGroups[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.ParameterGroups); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output.ParameterGroups[0], nil +} + func FindSubnetGroupByName(ctx context.Context, conn *memorydb.MemoryDB, name string) (*memorydb.SubnetGroup, error) { input := memorydb.DescribeSubnetGroupsInput{ SubnetGroupName: aws.String(name), diff --git a/internal/service/memorydb/parameter_group.go b/internal/service/memorydb/parameter_group.go new file mode 100644 index 00000000000..306f88fbcfe --- /dev/null +++ b/internal/service/memorydb/parameter_group.go @@ -0,0 +1,458 @@ +package memorydb + +import ( + "bytes" + "context" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/memorydb" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceParameterGroup() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceParameterGroupCreate, + ReadContext: resourceParameterGroupRead, + UpdateContext: resourceParameterGroupUpdate, + DeleteContext: resourceParameterGroupDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + CustomizeDiff: verify.SetTagsDiff, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "Managed by Terraform", + }, + "family": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 255), + validation.StringDoesNotMatch( + regexp.MustCompile(`[-][-]`), + "The name may not contain two consecutive hyphens."), + validation.StringMatch( + // Similar to ElastiCache, MemoryDB normalises names to lowercase. + regexp.MustCompile(`^[a-z0-9-]*[a-z0-9]$`), + "Only lowercase alphanumeric characters and hyphens allowed. The name may not end with a hyphen."), + ), + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 255-resource.UniqueIDSuffixLength), + validation.StringDoesNotMatch( + regexp.MustCompile(`[-][-]`), + "The name may not contain two consecutive hyphens."), + validation.StringMatch( + // Similar to ElastiCache, MemoryDB normalises names to lowercase. + regexp.MustCompile(`^[a-z0-9-]+$`), + "Only lowercase alphanumeric characters and hyphens allowed."), + ), + }, + "parameter": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: ParameterHash, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + } +} + +func resourceParameterGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).MemoryDBConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + name := create.Name(d.Get("name").(string), d.Get("name_prefix").(string)) + input := &memorydb.CreateParameterGroupInput{ + Description: aws.String(d.Get("description").(string)), + Family: aws.String(d.Get("family").(string)), + ParameterGroupName: aws.String(name), + Tags: Tags(tags.IgnoreAWS()), + } + + log.Printf("[DEBUG] Creating MemoryDB Parameter Group: %s", input) + output, err := conn.CreateParameterGroupWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error creating MemoryDB Parameter Group (%s): %s", name, err) + } + + d.SetId(name) + d.Set("arn", output.ParameterGroup.ARN) + + log.Printf("[INFO] MemoryDB Parameter Group ID: %s", d.Id()) + + // Update to apply parameter changes. + return resourceParameterGroupUpdate(ctx, d, meta) +} + +func resourceParameterGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).MemoryDBConn + + if d.HasChange("parameter") { + o, n := d.GetChange("parameter") + toRemove, toAdd := ParameterChanges(o, n) + + log.Printf("[DEBUG] Updating MemoryDB Parameter Group (%s)", d.Id()) + log.Printf("[DEBUG] Parameters to remove: %#v", toRemove) + log.Printf("[DEBUG] Parameters to add or update: %#v", toAdd) + + // The API is limited to updating no more than 20 parameters at a time. + const maxParams = 20 + + for len(toRemove) > 0 { + // Removing a parameter from state is equivalent to resetting it + // to its default state. + + var paramsToReset []*memorydb.ParameterNameValue + if len(toRemove) <= maxParams { + paramsToReset, toRemove = toRemove[:], nil + } else { + paramsToReset, toRemove = toRemove[:maxParams], toRemove[maxParams:] + } + + err := resetParameterGroupParameters(ctx, conn, d.Get("name").(string), paramsToReset) + + if err != nil { + return diag.Errorf("error resetting MemoryDB Parameter Group (%s) parameters to defaults: %s", d.Id(), err) + } + } + + for len(toAdd) > 0 { + var paramsToModify []*memorydb.ParameterNameValue + if len(toAdd) <= maxParams { + paramsToModify, toAdd = toAdd[:], nil + } else { + paramsToModify, toAdd = toAdd[:maxParams], toAdd[maxParams:] + } + + err := modifyParameterGroupParameters(ctx, conn, d.Get("name").(string), paramsToModify) + + if err != nil { + return diag.Errorf("error modifying MemoryDB Parameter Group (%s) parameters: %s", d.Id(), err) + } + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return diag.Errorf("error updating MemoryDB Parameter Group (%s) tags: %s", d.Id(), err) + } + } + + return resourceParameterGroupRead(ctx, d, meta) +} + +func resourceParameterGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).MemoryDBConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + group, err := FindParameterGroupByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] MemoryDB Parameter Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading MemoryDB Parameter Group (%s): %s", d.Id(), err) + } + + d.Set("arn", group.ARN) + d.Set("description", group.Description) + d.Set("family", group.Family) + d.Set("name", group.Name) + d.Set("name_prefix", create.NamePrefixFromName(aws.StringValue(group.Name))) + + userDefinedParameters := createUserDefinedParameterMap(d) + + parameters, err := listParameterGroupParameters(ctx, conn, d.Get("family").(string), d.Id(), userDefinedParameters) + if err != nil { + return diag.Errorf("error listing parameters for MemoryDB Parameter Group (%s): %s", d.Id(), err) + } + + if err := d.Set("parameter", flattenParameters(parameters)); err != nil { + return diag.Errorf("failed to set parameter: %s", err) + } + + tags, err := ListTags(conn, d.Get("arn").(string)) + + if err != nil { + return diag.Errorf("error listing tags for MemoryDB Parameter Group (%s): %s", d.Id(), err) + } + + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("error setting tags for MemoryDB Parameter Group (%s): %s", d.Id(), err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("error setting tags_all for MemoryDB Parameter Group (%s): %s", d.Id(), err) + } + + return nil +} + +func resourceParameterGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).MemoryDBConn + + log.Printf("[DEBUG] Deleting MemoryDB Parameter Group: (%s)", d.Id()) + _, err := conn.DeleteParameterGroupWithContext(ctx, &memorydb.DeleteParameterGroupInput{ + ParameterGroupName: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, memorydb.ErrCodeParameterGroupNotFoundFault) { + return nil + } + + if err != nil { + return diag.Errorf("error deleting MemoryDB Parameter Group (%s): %s", d.Id(), err) + } + + return nil +} + +// resetParameterGroupParameters resets the given parameters to their default values. +func resetParameterGroupParameters(ctx context.Context, conn *memorydb.MemoryDB, name string, parameters []*memorydb.ParameterNameValue) error { + var parameterNames []*string + for _, parameter := range parameters { + parameterNames = append(parameterNames, parameter.ParameterName) + } + + input := memorydb.ResetParameterGroupInput{ + ParameterGroupName: aws.String(name), + ParameterNames: parameterNames, + } + + return resource.Retry(30*time.Second, func() *resource.RetryError { + _, err := conn.ResetParameterGroupWithContext(ctx, &input) + if err != nil { + if tfawserr.ErrMessageContains(err, memorydb.ErrCodeInvalidParameterGroupStateFault, " has pending changes") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) +} + +// modifyParameterGroupParameters updates the given parameters. +func modifyParameterGroupParameters(ctx context.Context, conn *memorydb.MemoryDB, name string, parameters []*memorydb.ParameterNameValue) error { + input := memorydb.UpdateParameterGroupInput{ + ParameterGroupName: aws.String(name), + ParameterNameValues: parameters, + } + _, err := conn.UpdateParameterGroupWithContext(ctx, &input) + return err +} + +// listParameterGroupParameters returns the user-defined MemoryDB parameters +// in the group with the given name and family. +// +// Parameters given in userDefined will be returned even if the value is equal +// to the default. +func listParameterGroupParameters(ctx context.Context, conn *memorydb.MemoryDB, family, name string, userDefined map[string]string) ([]*memorydb.Parameter, error) { + query := func(ctx context.Context, parameterGroupName string) ([]*memorydb.Parameter, error) { + input := memorydb.DescribeParametersInput{ + ParameterGroupName: aws.String(parameterGroupName), + } + + output, err := conn.DescribeParametersWithContext(ctx, &input) + if err != nil { + return nil, err + } + + return output.Parameters, nil + } + + // There isn't an official API for defaults, and the mapping of family + // to default parameter group name is a guess. + + defaultsFamily := "default." + strings.ReplaceAll(family, "_", "-") + + defaults, err := query(ctx, defaultsFamily) + if err != nil { + return nil, fmt.Errorf("list defaults for family %s: %w", defaultsFamily, err) + } + + defaultValueByName := map[string]string{} + for _, defaultPV := range defaults { + defaultValueByName[aws.StringValue(defaultPV.Name)] = aws.StringValue(defaultPV.Value) + } + + current, err := query(ctx, name) + if err != nil { + return nil, err + } + + var result []*memorydb.Parameter + + for _, parameter := range current { + name := aws.StringValue(parameter.Name) + currentValue := aws.StringValue(parameter.Value) + defaultValue := defaultValueByName[name] + _, isUserDefined := userDefined[name] + + if currentValue != defaultValue || isUserDefined { + result = append(result, parameter) + } + } + + return result, nil +} + +// ParameterHash was copy-pasted from ElastiCache. +func ParameterHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) + + return create.StringHashcode(buf.String()) +} + +// ParameterChanges was copy-pasted from ElastiCache. +func ParameterChanges(o, n interface{}) (remove, addOrUpdate []*memorydb.ParameterNameValue) { + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + + om := make(map[string]*memorydb.ParameterNameValue, os.Len()) + for _, raw := range os.List() { + param := raw.(map[string]interface{}) + om[param["name"].(string)] = expandParameterNameValue(param) + } + nm := make(map[string]*memorydb.ParameterNameValue, len(addOrUpdate)) + for _, raw := range ns.List() { + param := raw.(map[string]interface{}) + nm[param["name"].(string)] = expandParameterNameValue(param) + } + + // Remove: key is in old, but not in new + remove = make([]*memorydb.ParameterNameValue, 0, os.Len()) + for k := range om { + if _, ok := nm[k]; !ok { + remove = append(remove, om[k]) + } + } + + // Add or Update: key is in new, but not in old or has changed value + addOrUpdate = make([]*memorydb.ParameterNameValue, 0, ns.Len()) + for k, nv := range nm { + ov, ok := om[k] + if !ok || ok && (aws.StringValue(nv.ParameterValue) != aws.StringValue(ov.ParameterValue)) { + addOrUpdate = append(addOrUpdate, nm[k]) + } + } + + return remove, addOrUpdate +} + +func flattenParameters(list []*memorydb.Parameter) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(list)) + for _, i := range list { + if i.Value != nil { + result = append(result, map[string]interface{}{ + "name": strings.ToLower(aws.StringValue(i.Name)), + "value": aws.StringValue(i.Value), + }) + } + } + return result +} + +func expandParameterNameValue(param map[string]interface{}) *memorydb.ParameterNameValue { + return &memorydb.ParameterNameValue{ + ParameterName: aws.String(param["name"].(string)), + ParameterValue: aws.String(param["value"].(string)), + } +} + +func createUserDefinedParameterMap(d *schema.ResourceData) map[string]string { + result := map[string]string{} + + for _, param := range d.Get("parameter").(*schema.Set).List() { + m, ok := param.(map[string]interface{}) + if !ok { + continue + } + + name, ok := m["name"].(string) + if !ok || name == "" { + continue + } + + value, ok := m["value"].(string) + if !ok || value == "" { + continue + } + + result[name] = value + } + + return result +} diff --git a/internal/service/memorydb/parameter_group_test.go b/internal/service/memorydb/parameter_group_test.go new file mode 100644 index 00000000000..49a6ed6327e --- /dev/null +++ b/internal/service/memorydb/parameter_group_test.go @@ -0,0 +1,526 @@ +package memorydb_test + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/memorydb" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfmemorydb "github.com/hashicorp/terraform-provider-aws/internal/service/memorydb" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccMemoryDBParameterGroup_basic(t *testing.T) { + rName := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_memorydb_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, memorydb.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckParameterGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "memorydb", "parametergroup/"+rName), + resource.TestCheckResourceAttr(resourceName, "description", "Managed by Terraform"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "family", "memorydb_redis6"), + resource.TestCheckResourceAttr(resourceName, "id", rName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "active-defrag-cycle-max", + "value": "70", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "active-defrag-cycle-min", + "value": "10", + }), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Test", "test"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccMemoryDBParameterGroup_disappears(t *testing.T) { + rName := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_memorydb_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, memorydb.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckParameterGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfmemorydb.ResourceParameterGroup(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccMemoryDBParameterGroup_update_parameters(t *testing.T) { + rName := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_memorydb_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, memorydb.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckParameterGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig_withParameter0(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withParameter1(rName, "timeout", "0"), // 0 is the default value + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "timeout", + "value": "0", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // Setting timeout to its default value will cause + // the import to diverge on the initial read. + ImportStateVerifyIgnore: []string{"parameter"}, + }, + { + Config: testAccParameterGroupConfig_withParameter1(rName, "timeout", "20"), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "timeout", + "value": "20", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withParameter2(rName, "timeout", "20", "activerehashing", "no"), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "timeout", + "value": "20", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "activerehashing", + "value": "no", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withParameter1(rName, "timeout", "20"), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "parameter.*", map[string]string{ + "name": "timeout", + "value": "20", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withParameter0(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "parameter.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccMemoryDBParameterGroup_update_tags(t *testing.T) { + rName := "tf-test-" + sdkacctest.RandString(8) + resourceName := "aws_memorydb_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, memorydb.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckSubnetGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig_withTags0(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withTags2(rName, "Key1", "value1", "Key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key2", "value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withTags1(rName, "Key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.Key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccParameterGroupConfig_withTags0(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckParameterGroupDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).MemoryDBConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_memorydb_parameter_group" { + continue + } + + _, err := tfmemorydb.FindParameterGroupByName(context.Background(), conn, rs.Primary.Attributes["name"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("MemoryDB Parameter Group %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckParameterGroupExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No MemoryDB Parameter Group ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).MemoryDBConn + + _, err := tfmemorydb.FindParameterGroupByName(context.Background(), conn, rs.Primary.Attributes["name"]) + + if err != nil { + return err + } + + return nil + } +} + +func testAccParameterGroupConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" + + parameter { + name = "active-defrag-cycle-max" + value = "70" + } + + parameter { + name = "active-defrag-cycle-min" + value = "10" + } + + tags = { + Test = "test" + } +} +`, rName) +} + +func testAccParameterGroupConfig_withParameter0(rName string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" +} +`, rName) +} + +func testAccParameterGroupConfig_withParameter1(rName, paramName1, paramValue1 string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" + + parameter { + name = %[2]q + value = %[3]q + } +} +`, rName, paramName1, paramValue1) +} + +func testAccParameterGroupConfig_withParameter2(rName, paramName1, paramValue1, paramName2, paramValue2 string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" + + parameter { + name = %[2]q + value = %[3]q + } + + parameter { + name = %[4]q + value = %[5]q + } +} +`, rName, paramName1, paramValue1, paramName2, paramValue2) +} + +func testAccParameterGroupConfig_withTags0(rName string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" +} +`, rName) +} + +func testAccParameterGroupConfig_withTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccParameterGroupConfig_withTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_memorydb_parameter_group" "test" { + name = %[1]q + family = "memorydb_redis6" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +// TestParameterChanges was copy-pasted from the ElastiCache implementation. +func TestParameterChanges(t *testing.T) { + cases := []struct { + Name string + Old *schema.Set + New *schema.Set + ExpectedRemove []*memorydb.ParameterNameValue + ExpectedAddOrUpdate []*memorydb.ParameterNameValue + }{ + { + Name: "Empty", + Old: new(schema.Set), + New: new(schema.Set), + ExpectedRemove: []*memorydb.ParameterNameValue{}, + ExpectedAddOrUpdate: []*memorydb.ParameterNameValue{}, + }, + { + Name: "Remove all", + Old: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "reserved-memory", + "value": "0", + }, + }), + New: new(schema.Set), + ExpectedRemove: []*memorydb.ParameterNameValue{ + { + ParameterName: aws.String("reserved-memory"), + ParameterValue: aws.String("0"), + }, + }, + ExpectedAddOrUpdate: []*memorydb.ParameterNameValue{}, + }, + { + Name: "No change", + Old: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "reserved-memory", + "value": "0", + }, + }), + New: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "reserved-memory", + "value": "0", + }, + }), + ExpectedRemove: []*memorydb.ParameterNameValue{}, + ExpectedAddOrUpdate: []*memorydb.ParameterNameValue{}, + }, + { + Name: "Remove partial", + Old: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "reserved-memory", + "value": "0", + }, + map[string]interface{}{ + "name": "appendonly", + "value": "yes", + }, + }), + New: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "appendonly", + "value": "yes", + }, + }), + ExpectedRemove: []*memorydb.ParameterNameValue{ + { + ParameterName: aws.String("reserved-memory"), + ParameterValue: aws.String("0"), + }, + }, + ExpectedAddOrUpdate: []*memorydb.ParameterNameValue{}, + }, + { + Name: "Add to existing", + Old: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "appendonly", + "value": "yes", + }, + }), + New: schema.NewSet(tfmemorydb.ParameterHash, []interface{}{ + map[string]interface{}{ + "name": "appendonly", + "value": "yes", + }, + map[string]interface{}{ + "name": "appendfsync", + "value": "always", + }, + }), + ExpectedRemove: []*memorydb.ParameterNameValue{}, + ExpectedAddOrUpdate: []*memorydb.ParameterNameValue{ + { + ParameterName: aws.String("appendfsync"), + ParameterValue: aws.String("always"), + }, + }, + }, + } + + for _, tc := range cases { + remove, addOrUpdate := tfmemorydb.ParameterChanges(tc.Old, tc.New) + if !reflect.DeepEqual(remove, tc.ExpectedRemove) { + t.Errorf("Case %q: Remove did not match\n%#v\n\nGot:\n%#v", tc.Name, tc.ExpectedRemove, remove) + } + if !reflect.DeepEqual(addOrUpdate, tc.ExpectedAddOrUpdate) { + t.Errorf("Case %q: AddOrUpdate did not match\n%#v\n\nGot:\n%#v", tc.Name, tc.ExpectedAddOrUpdate, addOrUpdate) + } + } +} diff --git a/website/docs/r/memorydb_parameter_group.html.markdown b/website/docs/r/memorydb_parameter_group.html.markdown new file mode 100644 index 00000000000..8a7641ffe03 --- /dev/null +++ b/website/docs/r/memorydb_parameter_group.html.markdown @@ -0,0 +1,59 @@ +--- +subcategory: "MemoryDB" +layout: "aws" +page_title: "AWS: aws_memorydb_parameter_group" +description: |- + Provides a MemoryDB Parameter Group. +--- + +# Resource: aws_memorydb_parameter_group + +Provides a MemoryDB Parameter Group. + +More information about parameter groups can be found in the [MemoryDB User Guide](https://docs.aws.amazon.com/memorydb/latest/devguide/parametergroups.html). + +## Example Usage + +```terraform +resource "aws_memorydb_parameter_group" "example" { + name = "my-parameter-group" + family = "memorydb_redis6" + + parameter { + name = "activedefrag" + value = "yes" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Optional, Forces new resource) Name of the parameter group. If omitted, Terraform will assign a random, unique name. Conflicts with `name_prefix`. +* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`. +* `description` - (Optional, Forces new resource) Description for the parameter group. Defaults to `"Managed by Terraform"`. +* `family` - (Required, Forces new resource) The engine version that the parameter group can be used with. +* `parameter` - (Optional) Set of MemoryDB parameters to apply. Any parameters not specified will fall back to their family defaults. Detailed below. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### parameter Configuration Block + +* `name` - (Required) The name of the parameter. +* `value` - (Required) The value of the parameter. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Same as `name`. +* `arn` - The ARN of the parameter group. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Import + +Use the `name` to import a parameter group. For example: + +``` +$ terraform import aws_memorydb_parameter_group.example my-parameter-group +```