provider/aws: Make the type of a route53_record changeable

Utilize the ChangeResourceRecordSets to change the type of a record by
deleting and recreating with a new type.

As change batches are considered transactional changes, Amazon Route 53
either makes all or none of the changes in the batch request ensuring the
update will never be partially applied.
This commit is contained in:
Johannes Würbach 2017-01-12 00:08:20 +01:00
parent 1f036e7743
commit ef0196f754
No known key found for this signature in database
GPG Key ID: 74DB0F4D956CCCE3
2 changed files with 244 additions and 9 deletions

View File

@ -49,7 +49,6 @@ func resourceAwsRoute53Record() *schema.Resource {
"type": &schema.Schema{ "type": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true,
ValidateFunc: validateRoute53RecordType, ValidateFunc: validateRoute53RecordType,
}, },
@ -81,7 +80,6 @@ func resourceAwsRoute53Record() *schema.Resource {
"set_identifier": &schema.Schema{ "set_identifier": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true,
}, },
"alias": &schema.Schema{ "alias": &schema.Schema{
@ -223,13 +221,124 @@ func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) er
// Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and // Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and
// AWS dynamically determines if a record should be created or updated. // AWS dynamically determines if a record should be created or updated.
// Amazon Route 53 can update an existing resource record set only when all // Amazon Route 53 can update an existing resource record set only when all
// of the following values match: Name, Type // of the following values match: Name, Type and SetIdentifier
// (and SetIdentifier, which we don't use yet). // See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html
// See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets_Requests.html#change-rrsets-request-action
// if !d.HasChange("type") && !d.HasChange("set_identifier") {
// Because we use UPSERT, for resouce update here we simply fall through to // If neither type nor set_identifier changed we use UPSERT,
// our resource create function. // for resouce update here we simply fall through to
return resourceAwsRoute53RecordCreate(d, meta) // our resource create function.
return resourceAwsRoute53RecordCreate(d, meta)
}
// Otherwise we delete the existing record and create a new record within
// a transactional change
conn := meta.(*AWSClient).r53conn
zone := cleanZoneID(d.Get("zone_id").(string))
var err error
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)})
if err != nil {
return err
}
if zoneRecord.HostedZone == nil {
return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone)
}
// Build the to be deleted record
en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
typeo, _ := d.GetChange("type")
oldRec := &route53.ResourceRecordSet{
Name: aws.String(en),
Type: aws.String(typeo.(string)),
}
if v, _ := d.GetChange("ttl"); v.(int) != 0 {
oldRec.TTL = aws.Int64(int64(v.(int)))
}
// Resource records
if v, _ := d.GetChange("records"); v != nil {
recs := v.(*schema.Set).List()
if len(recs) > 0 {
oldRec.ResourceRecords = expandResourceRecords(recs, typeo.(string))
}
}
// Alias record
if v, _ := d.GetChange("alias"); v != nil {
aliases := v.(*schema.Set).List()
if len(aliases) == 1 {
alias := aliases[0].(map[string]interface{})
oldRec.AliasTarget = &route53.AliasTarget{
DNSName: aws.String(alias["name"].(string)),
EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)),
HostedZoneId: aws.String(alias["zone_id"].(string)),
}
}
}
if v, _ := d.GetChange("set_identifier"); v.(string) != "" {
oldRec.SetIdentifier = aws.String(v.(string))
}
// Build the to be created record
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
if err != nil {
return err
}
// Delete the old and create the new records in a single batch. We abuse
// StateChangeConf for this to retry for us since Route53 sometimes returns
// errors about another operation happening at the same time.
changeBatch := &route53.ChangeBatch{
Comment: aws.String("Managed by Terraform"),
Changes: []*route53.Change{
&route53.Change{
Action: aws.String("DELETE"),
ResourceRecordSet: oldRec,
},
&route53.Change{
Action: aws.String("CREATE"),
ResourceRecordSet: rec,
},
},
}
req := &route53.ChangeResourceRecordSetsInput{
HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)),
ChangeBatch: changeBatch,
}
log.Printf("[DEBUG] Updating resource records for zone: %s, name: %s\n\n%s",
zone, *rec.Name, req)
respRaw, err := changeRoute53RecordSet(conn, req)
if err != nil {
return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err)
}
changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo
// Generate an ID
vars := []string{
zone,
strings.ToLower(d.Get("name").(string)),
d.Get("type").(string),
}
if v, ok := d.GetOk("set_identifier"); ok {
vars = append(vars, v.(string))
}
d.SetId(strings.Join(vars, "_"))
err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id))
if err != nil {
return err
}
return resourceAwsRoute53RecordRead(d, meta)
} }
func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error {

View File

@ -339,6 +339,56 @@ func TestAccAWSRoute53Record_TypeChange(t *testing.T) {
}) })
} }
func TestAccAWSRoute53Record_SetIdentiferChange(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_route53_record.basic_to_weighted",
Providers: testAccProviders,
CheckDestroy: testAccCheckRoute53RecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRoute53RecordSetIdentifierChangePre,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.basic_to_weighted"),
),
},
// Cause a change, which will trigger a refresh
resource.TestStep{
Config: testAccRoute53RecordSetIdentifierChangePost,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.basic_to_weighted"),
),
},
},
})
}
func TestAccAWSRoute53Record_AliasChange(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_route53_record.elb_alias_change",
Providers: testAccProviders,
CheckDestroy: testAccCheckRoute53RecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRoute53RecordAliasChangePre,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.elb_alias_change"),
),
},
// Cause a change, which will trigger a refresh
resource.TestStep{
Config: testAccRoute53RecordAliasChangePost,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.elb_alias_change"),
),
},
},
})
}
func TestAccAWSRoute53Record_empty(t *testing.T) { func TestAccAWSRoute53Record_empty(t *testing.T) {
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -1022,6 +1072,82 @@ resource "aws_route53_record" "sample" {
} }
` `
const testAccRoute53RecordSetIdentifierChangePre = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_route53_record" "basic_to_weighted" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "sample"
type = "A"
ttl = "30"
records = ["127.0.0.1", "8.8.8.8"]
}
`
const testAccRoute53RecordSetIdentifierChangePost = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_route53_record" "basic_to_weighted" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "sample"
type = "A"
ttl = "30"
records = ["127.0.0.1", "8.8.8.8"]
set_identifier = "cluster-a"
weighted_routing_policy {
weight = 100
}
}
`
const testAccRoute53RecordAliasChangePre = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_elb" "alias_change" {
name = "foobar-tf-elb-alias-change"
availability_zones = ["us-west-2a"]
listener {
instance_port = 80
instance_protocol = "http"
lb_port = 80
lb_protocol = "http"
}
}
resource "aws_route53_record" "elb_alias_change" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "alias-change"
type = "A"
alias {
zone_id = "${aws_elb.alias_change.zone_id}"
name = "${aws_elb.alias_change.dns_name}"
evaluate_target_health = true
}
}
`
const testAccRoute53RecordAliasChangePost = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_route53_record" "elb_alias_change" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "alias-change"
type = "CNAME"
ttl = "30"
records = ["www.terraform.io"]
}
`
const testAccRoute53RecordConfigEmptyName = ` const testAccRoute53RecordConfigEmptyName = `
resource "aws_route53_zone" "main" { resource "aws_route53_zone" "main" {
name = "notexample.com" name = "notexample.com"