Added new aws_db_parameter_group resource for creating RDS DB Parameter Groups.

Added plumbing to allow an optional DBParameterGroupName to be passed when creating an RDS DB instance.
This commit is contained in:
Rob Costanzo 2014-10-22 14:22:30 -07:00
parent ecf66ad7b5
commit de16b10555
10 changed files with 570 additions and 0 deletions

View File

@ -50,6 +50,7 @@ func Provider() *schema.Provider {
"aws_security_group": resourceAwsSecurityGroup(),
"aws_db_subnet_group": resourceAwsDbSubnetGroup(),
"aws_vpc": resourceAwsVpc(),
"aws_db_parameter_group": resourceAwsDbParameterGroup(),
},
}
}

View File

@ -79,6 +79,10 @@ func resource_aws_db_instance_create(
opts.DBSubnetGroupName = attr
}
if attr = rs.Attributes["parameter_group_name"]; attr != "" {
opts.DBParameterGroupName = attr
}
if err != nil {
return nil, fmt.Errorf("Error parsing configuration: %s", err)
}
@ -227,6 +231,7 @@ func resource_aws_db_instance_diff(
"vpc_security_group_ids": diff.AttrTypeCreate,
"security_group_names": diff.AttrTypeCreate,
"db_subnet_group_name": diff.AttrTypeCreate,
"parameter_group_name": diff.AttrTypeCreate,
"skip_final_snapshot": diff.AttrTypeUpdate,
"final_snapshot_identifier": diff.AttrTypeUpdate,
},
@ -270,6 +275,7 @@ func resource_aws_db_instance_update_state(
s.Attributes["status"] = v.DBInstanceStatus
s.Attributes["username"] = v.MasterUsername
s.Attributes["db_subnet_group_name"] = v.DBSubnetGroup.Name
s.Attributes["parameter_group_name"] = v.DBParameterGroupName
// Flatten our group values
toFlatten := make(map[string]interface{})
@ -340,6 +346,7 @@ func resource_aws_db_instance_validation() *config.Validator {
"skip_final_snapshot",
"security_group_names.*",
"db_subnet_group_name",
"parameter_group_name",
},
}
}

View File

@ -43,6 +43,8 @@ func TestAccAWSDBInstance(t *testing.T) {
"aws_db_instance.bar", "skip_final_snapshot", "true"),
resource.TestCheckResourceAttr(
"aws_db_instance.bar", "security_group_names.0", "secfoobarbaz-test-terraform"),
resource.TestCheckResourceAttr(
"aws_db_instance.bar", "parameter_group_name", "default.mysql5.6"),
),
},
},
@ -160,5 +162,6 @@ resource "aws_db_instance" "bar" {
skip_final_snapshot = true
security_group_names = ["${aws_db_security_group.bar.name}"]
parameter_group_name = "default.mysql5.6"
}
`

View File

@ -0,0 +1,176 @@
package aws
import (
"fmt"
"log"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/rds"
)
func resourceAwsDbParameterGroup() *schema.Resource {
return &schema.Resource{
Create: resourceAwsDbParameterGroupCreate,
Read: resourceAwsDbParameterGroupRead,
Update: nil,
Delete: resourceAwsDbParameterGroupDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"family": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"parameter": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"apply_method": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"value": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
},
},
}
}
func resourceAwsDbParameterGroupCreate(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider)
rdsconn := p.rdsconn
createOpts := rds.CreateDBParameterGroup{
DBParameterGroupName: d.Get("name").(string),
DBParameterGroupFamily: d.Get("family").(string),
Description: d.Get("description").(string),
}
log.Printf("[DEBUG] Create DB Parameter Group: %#v", createOpts)
_, err := rdsconn.CreateDBParameterGroup(&createOpts)
if err != nil {
return fmt.Errorf("Error creating DB Parameter Group: %s", err)
}
d.SetId(createOpts.DBParameterGroupName)
log.Printf("[INFO] DB Parameter Group ID: %s", d.Id())
if d.Get("parameter") != "" {
// Expand the "parameter" set to goamz compat []rds.Parameter
parameters, err := expandParameters(d.Get("parameter").([]interface{}))
if err != nil {
return err
}
modifyOpts := rds.ModifyDBParameterGroup{
DBParameterGroupName: d.Get("name").(string),
Parameters: parameters,
}
log.Printf("[DEBUG] Modify DB Parameter Group: %#v", modifyOpts)
_, err = rdsconn.ModifyDBParameterGroup(&modifyOpts)
if err != nil {
return fmt.Errorf("Error modifying DB Parameter Group: %s", err)
}
}
return resourceAwsDbParameterGroupRead(d, meta)
}
func resourceAwsDbParameterGroupDelete(d *schema.ResourceData, meta interface{}) error {
stateConf := &resource.StateChangeConf{
Pending: []string{"pending"},
Target: "destroyed",
Refresh: resourceDbParameterGroupDeleteRefreshFunc(d, meta),
Timeout: 3 * time.Minute,
MinTimeout: 1 * time.Second,
}
_, err := stateConf.WaitForState()
return err
}
func resourceAwsDbParameterGroupRead(d *schema.ResourceData, meta interface{}) error {
p := meta.(*ResourceProvider)
rdsconn := p.rdsconn
describeOpts := rds.DescribeDBParameterGroups{
DBParameterGroupName: d.Id(),
}
describeResp, err := rdsconn.DescribeDBParameterGroups(&describeOpts)
if err != nil {
return err
}
if len(describeResp.DBParameterGroups) != 1 ||
describeResp.DBParameterGroups[0].DBParameterGroupName != d.Id() {
return fmt.Errorf("Unable to find Parameter Group: %#v", describeResp.DBParameterGroups)
}
d.Set("name", describeResp.DBParameterGroups[0].DBParameterGroupName)
d.Set("family", describeResp.DBParameterGroups[0].DBParameterGroupFamily)
d.Set("description", describeResp.DBParameterGroups[0].Description)
// Only include user customized parameters as there's hundreds of system/default ones
describeParametersOpts := rds.DescribeDBParameters{
DBParameterGroupName: d.Id(),
Source: "user",
}
describeParametersResp, err := rdsconn.DescribeDBParameters(&describeParametersOpts)
if err != nil {
return err
}
d.Set("parameter", flattenParameters(describeParametersResp.Parameters))
return nil
}
func resourceDbParameterGroupDeleteRefreshFunc(
d *schema.ResourceData,
meta interface{}) resource.StateRefreshFunc {
p := meta.(*ResourceProvider)
rdsconn := p.rdsconn
return func() (interface{}, string, error) {
deleteOpts := rds.DeleteDBParameterGroup{
DBParameterGroupName: d.Id(),
}
if _, err := rdsconn.DeleteDBParameterGroup(&deleteOpts); err != nil {
rdserr, ok := err.(*rds.Error)
if !ok {
return d, "error", err
}
if rdserr.Code != "DBParameterGroupNotFoundFault" {
return d, "error", err
}
}
return d, "destroyed", nil
}
}

View File

@ -0,0 +1,218 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/goamz/rds"
)
func TestAccAWSDBParameterGroup(t *testing.T) {
var v rds.DBParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBParameterGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSDBParameterGroupConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBParameterGroupExists("aws_db_parameter_group.bar", &v),
testAccCheckAWSDBParameterGroupAttributes(&v),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "name", "parameter-group-test-terraform"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "family", "mysql5.6"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "description", "Test parameter group for terraform"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.0.name", "character_set_client"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.0.value", "utf8"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.0.apply_method", "immediate"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.1.name", "character_set_results"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.1.value", "utf8"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.1.apply_method", "immediate"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.2.name", "character_set_server"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.2.value", "utf8"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.2.apply_method", "immediate"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.3.name", "collation_connection"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.3.value", "utf8_unicode_ci"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.3.apply_method", "immediate"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.4.name", "collation_server"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.4.value", "utf8_unicode_ci"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "parameter.4.apply_method", "immediate"),
),
},
},
})
}
func TestAccAWSDBParameterGroupOnly(t *testing.T) {
var v rds.DBParameterGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBParameterGroupDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSDBParameterGroupOnlyConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBParameterGroupExists("aws_db_parameter_group.bar", &v),
testAccCheckAWSDBParameterGroupAttributes(&v),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "name", "parameter-group-test-terraform"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "family", "mysql5.6"),
resource.TestCheckResourceAttr(
"aws_db_parameter_group.bar", "description", "Test parameter group for terraform"),
),
},
},
})
}
func testAccCheckAWSDBParameterGroupDestroy(s *terraform.State) error {
conn := testAccProvider.rdsconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_db_parameter_group" {
continue
}
// Try to find the Group
resp, err := conn.DescribeDBParameterGroups(
&rds.DescribeDBParameterGroups{
DBParameterGroupName: rs.Primary.ID,
})
if err == nil {
if len(resp.DBParameterGroups) != 0 &&
resp.DBParameterGroups[0].DBParameterGroupName == rs.Primary.ID {
return fmt.Errorf("DB Parameter Group still exists")
}
}
// Verify the error
newerr, ok := err.(*rds.Error)
if !ok {
return err
}
if newerr.Code != "InvalidDBParameterGroup.NotFound" {
return err
}
}
return nil
}
func testAccCheckAWSDBParameterGroupAttributes(v *rds.DBParameterGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
if v.DBParameterGroupName != "parameter-group-test-terraform" {
return fmt.Errorf("bad name: %#v", v.DBParameterGroupName)
}
if v.DBParameterGroupFamily != "mysql5.6" {
return fmt.Errorf("bad family: %#v", v.DBParameterGroupFamily)
}
if v.Description != "Test parameter group for terraform" {
return fmt.Errorf("bad description: %#v", v.Description)
}
return nil
}
}
func testAccCheckAWSDBParameterGroupExists(n string, v *rds.DBParameterGroup) 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 DB Parameter Group ID is set")
}
conn := testAccProvider.rdsconn
opts := rds.DescribeDBParameterGroups{
DBParameterGroupName: rs.Primary.ID,
}
resp, err := conn.DescribeDBParameterGroups(&opts)
if err != nil {
return err
}
if len(resp.DBParameterGroups) != 1 ||
resp.DBParameterGroups[0].DBParameterGroupName != rs.Primary.ID {
return fmt.Errorf("DB Parameter Group not found")
}
*v = resp.DBParameterGroups[0]
return nil
}
}
const testAccAWSDBParameterGroupConfig = `
resource "aws_db_parameter_group" "bar" {
name = "parameter-group-test-terraform"
family = "mysql5.6"
description = "Test parameter group for terraform"
parameter {
name = "character_set_server"
value = "utf8"
apply_method = "immediate"
}
parameter {
name = "character_set_client"
value = "utf8"
apply_method = "immediate"
}
parameter{
name = "character_set_results"
value = "utf8"
apply_method = "immediate"
}
parameter {
name = "collation_server"
value = "utf8_unicode_ci"
apply_method = "immediate"
}
parameter {
name = "collation_connection"
value = "utf8_unicode_ci"
apply_method = "immediate"
}
}
`
const testAccAWSDBParameterGroupOnlyConfig = `
resource "aws_db_parameter_group" "bar" {
name = "parameter-group-test-terraform"
family = "mysql5.6"
description = "Test parameter group for terraform"
}
`

View File

@ -1,11 +1,13 @@
package aws
import (
"sort"
"strings"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/goamz/elb"
"github.com/mitchellh/goamz/rds"
)
// Takes the result of flatmap.Expand for an array of listeners and
@ -87,6 +89,28 @@ func expandIPPerms(id string, configured []interface{}) []ec2.IPPerm {
return perms
}
// Takes the result of flatmap.Expand for an array of parameters and
// returns Parameter API compatible objects
func expandParameters(configured []interface{}) ([]rds.Parameter, error) {
parameters := make([]rds.Parameter, 0, len(configured))
// Loop over our configured parameters and create
// an array of goamz compatabile objects
for _, pRaw := range configured {
data := pRaw.(map[string]interface{})
p := rds.Parameter{
ApplyMethod: data["apply_method"].(string),
ParameterName: data["name"].(string),
ParameterValue: data["value"].(string),
}
parameters = append(parameters, p)
}
return parameters, nil
}
// Flattens an array of ipPerms into a list of primitives that
// flatmap.Flatten() can handle
func flattenIPPerms(list []ec2.IPPerm) []map[string]interface{} {
@ -162,6 +186,19 @@ func flattenListeners(list []elb.Listener) []map[string]interface{} {
return result
}
// Flattens and sorts by name an array of Parameters into a []map[string]interface{}
func flattenParameters(list []rds.Parameter) []map[string]interface{} {
sort.Sort(ByParameterName(list))
result := make([]map[string]interface{}, 0, len(list))
for _, i := range list {
result = append(result, map[string]interface{}{
"name": strings.ToLower(i.ParameterName),
"value": strings.ToLower(i.ParameterValue),
})
}
return result
}
// Takes the result of flatmap.Expand for an array of strings
// and returns a []string
func expandStringList(configured []interface{}) []string {
@ -171,3 +208,8 @@ func expandStringList(configured []interface{}) []string {
}
return vs
}
type ByParameterName []rds.Parameter
func (a ByParameterName) Len() int { return len(a) }
func (a ByParameterName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByParameterName) Less(i, j int) bool { return a[i].ParameterName < a[j].ParameterName }

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/goamz/elb"
"github.com/mitchellh/goamz/rds"
)
// Returns test configuration
@ -254,3 +255,67 @@ func Test_expandStringList(t *testing.T) {
}
}
func Test_expandParameters(t *testing.T) {
expanded := []interface{}{
map[string]interface{}{
"name": "character_set_client",
"value": "utf8",
"apply_method": "immediate",
},
}
parameters, err := expandParameters(expanded)
if err != nil {
t.Fatalf("bad: %#v", err)
}
expected := rds.Parameter{
ParameterName: "character_set_client",
ParameterValue: "utf8",
ApplyMethod: "immediate",
}
if !reflect.DeepEqual(parameters[0], expected) {
t.Fatalf(
"Got:\n\n%#v\n\nExpected:\n\n%#v\n",
parameters[0],
expected)
}
}
func Test_flattenParameters(t *testing.T) {
cases := []struct {
Input []rds.Parameter
Output []map[string]interface{}
}{
{
Input: []rds.Parameter{
rds.Parameter{
ParameterName: "character_set_client",
ParameterValue: "utf8",
},
rds.Parameter{
ParameterName: "character_set_aaaclient",
ParameterValue: "utf8",
},
},
Output: []map[string]interface{}{
map[string]interface{}{
"name": "character_set_aaaclient",
"value": "utf8",
},
map[string]interface{}{
"name": "character_set_client",
"value": "utf8",
},
},
},
}
for _, tc := range cases {
output := flattenParameters(tc.Input)
if !reflect.DeepEqual(output, tc.Output) {
t.Fatalf("Got:\n\n%#v\n\nExpected:\n\n%#v", output, tc.Output)
}
}
}

View File

@ -24,6 +24,7 @@ resource "aws_db_instance" "default" {
password = "bar"
security_group_names = ["${aws_db_security_group.bar.name}"]
db_subnet_group_name = "my_database_subnet_group"
parameter_group_name = "default.mysql5.6"
}
```
@ -53,6 +54,7 @@ The following arguments are supported:
* `skip_final_snapshot` - (Optional) Enables skipping the final snapshot on deletion.
* `security_group_names` - (Optional) List of DB Security Groups to associate.
* `db_subnet_group_name` - (Optional) Name of DB subnet group
* `parameter_group_name` - (Optional) Name of the DB parameter group to associate.
## Attributes Reference

View File

@ -0,0 +1,52 @@
---
layout: "aws"
page_title: "AWS: aws_db_parameter_group"
sidebar_current: "docs-aws-resource-db-parameter-group"
---
# aws\_db\_parameter\_group
Provides an RDS DB parameter group resource.
## Example Usage
```
resource "aws_db_parameter_group" "default" {
name = "rds_pg"
family = "mysql5.6"
description = "RDS default parameter group"
parameter {
name = "character_set_server"
value = "utf8"
apply_method = "immediate"
}
parameter {
name = "character_set_client"
value = "utf8"
apply_method = "immediate"
}
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the DB parameter group.
* `family` - (Required) The family of the DB parameter group.
* `description` - (Required) The description of the DB parameter group.
* `parameter` - (Optional) A list of DB parameters to apply.
Parameter blocks support the following:
* `name` - (Required) The name of the DB parameter.
* `value` - (Required) The value of the DB parameter.
* `apply_method` - (Required) Indicates when to apply parameter updates. Either immediate or pending-reboot.
## Attributes Reference
The following attributes are exported:
* `id` - The db parameter group name.

View File

@ -29,6 +29,10 @@
<a href="/docs/providers/aws/r/db_subnet_group.html">aws_db_subnet_group</a>
</li>
<li<%= sidebar_current("docs-aws-resource-db-parameter-group") %>>
<a href="/docs/providers/aws/r/db_parameter_group.html">aws_db_parameter_group</a>
</li>
<li<%= sidebar_current("docs-aws-resource-eip") %>>
<a href="/docs/providers/aws/r/eip.html">aws_eip</a>
</li>