2014-07-23 05:08:39 +02:00
package aws
import (
2015-05-01 12:02:06 +02:00
"bytes"
2016-01-29 18:56:19 +01:00
"errors"
2014-07-23 05:08:39 +02:00
"fmt"
"log"
"strings"
"time"
2016-08-23 19:25:09 +02:00
"github.com/hashicorp/errwrap"
2014-12-16 00:40:43 +01:00
"github.com/hashicorp/terraform/helper/hashcode"
2014-07-23 05:08:39 +02:00
"github.com/hashicorp/terraform/helper/resource"
2014-11-21 17:58:34 +01:00
"github.com/hashicorp/terraform/helper/schema"
2015-02-12 17:48:48 +01:00
2015-06-03 20:36:57 +02:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/route53"
2014-07-23 05:08:39 +02:00
)
2016-01-29 18:56:19 +01:00
var r53NoRecordsFound = errors . New ( "No matching Hosted Zone found" )
var r53NoHostedZoneFound = errors . New ( "No matching records found" )
2014-11-21 17:58:34 +01:00
func resourceAwsRoute53Record ( ) * schema . Resource {
return & schema . Resource {
Create : resourceAwsRoute53RecordCreate ,
Read : resourceAwsRoute53RecordRead ,
2015-04-06 17:16:23 +02:00
Update : resourceAwsRoute53RecordUpdate ,
2014-11-21 17:58:34 +01:00
Delete : resourceAwsRoute53RecordDelete ,
2017-01-11 23:38:02 +01:00
Importer : & schema . ResourceImporter {
State : schema . ImportStatePassthrough ,
} ,
2015-08-12 18:03:47 +02:00
SchemaVersion : 2 ,
2016-05-09 23:48:16 +02:00
MigrateState : resourceAwsRoute53RecordMigrateState ,
2014-11-21 17:58:34 +01:00
Schema : map [ string ] * schema . Schema {
2017-03-16 19:57:21 +01:00
"name" : {
2014-11-21 17:58:34 +01:00
Type : schema . TypeString ,
Required : true ,
ForceNew : true ,
2015-10-20 20:32:35 +02:00
StateFunc : func ( v interface { } ) string {
2016-05-09 23:48:16 +02:00
value := strings . TrimSuffix ( v . ( string ) , "." )
2015-10-20 20:32:35 +02:00
return strings . ToLower ( value )
} ,
2014-11-21 17:58:34 +01:00
} ,
2017-03-16 19:57:21 +01:00
"fqdn" : {
2015-05-07 13:54:53 +02:00
Type : schema . TypeString ,
Computed : true ,
} ,
2017-03-16 19:57:21 +01:00
"type" : {
2017-01-09 21:45:26 +01:00
Type : schema . TypeString ,
Required : true ,
ValidateFunc : validateRoute53RecordType ,
2014-11-21 17:58:34 +01:00
} ,
2017-03-16 19:57:21 +01:00
"zone_id" : {
2014-11-21 17:58:34 +01:00
Type : schema . TypeString ,
Required : true ,
ForceNew : true ,
2015-11-16 23:58:11 +01:00
ValidateFunc : func ( v interface { } , k string ) ( ws [ ] string , es [ ] error ) {
value := v . ( string )
if value == "" {
es = append ( es , fmt . Errorf ( "Cannot have empty zone_id" ) )
}
return
} ,
2014-11-21 17:58:34 +01:00
} ,
2017-03-16 19:57:21 +01:00
"ttl" : {
2015-05-01 12:02:06 +02:00
Type : schema . TypeInt ,
Optional : true ,
ConflictsWith : [ ] string { "alias" } ,
2014-11-21 17:58:34 +01:00
} ,
2017-03-16 19:57:21 +01:00
"weight" : {
2015-04-17 19:02:05 +02:00
Type : schema . TypeInt ,
Optional : true ,
2016-05-31 22:24:46 +02:00
Removed : "Now implemented as weighted_routing_policy; Please see https://www.terraform.io/docs/providers/aws/r/route53_record.html" ,
2015-04-17 19:02:05 +02:00
} ,
2017-03-16 19:57:21 +01:00
"set_identifier" : {
2015-04-17 19:02:05 +02:00
Type : schema . TypeString ,
Optional : true ,
} ,
2017-03-16 19:57:21 +01:00
"alias" : {
2015-05-01 12:02:06 +02:00
Type : schema . TypeSet ,
Optional : true ,
ConflictsWith : [ ] string { "records" , "ttl" } ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
2017-03-16 19:57:21 +01:00
"zone_id" : {
2015-05-01 12:02:06 +02:00
Type : schema . TypeString ,
Required : true ,
} ,
2017-03-16 19:57:21 +01:00
"name" : {
2016-10-31 20:18:00 +01:00
Type : schema . TypeString ,
Required : true ,
StateFunc : normalizeAwsAliasName ,
2015-05-01 12:02:06 +02:00
} ,
2017-03-16 19:57:21 +01:00
"evaluate_target_health" : {
2015-05-01 12:02:06 +02:00
Type : schema . TypeBool ,
Required : true ,
} ,
} ,
} ,
Set : resourceAwsRoute53AliasRecordHash ,
} ,
2017-03-16 19:57:21 +01:00
"failover" : { // PRIMARY | SECONDARY
2015-04-28 18:11:38 +02:00
Type : schema . TypeString ,
Optional : true ,
2015-08-12 18:03:47 +02:00
Removed : "Now implemented as failover_routing_policy; see docs" ,
} ,
2017-03-16 19:57:21 +01:00
"failover_routing_policy" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeList ,
Optional : true ,
ConflictsWith : [ ] string {
"geolocation_routing_policy" ,
"latency_routing_policy" ,
"weighted_routing_policy" ,
} ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
2017-03-16 19:57:21 +01:00
"type" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeString ,
Required : true ,
ValidateFunc : func ( v interface { } , k string ) ( ws [ ] string , es [ ] error ) {
value := v . ( string )
if value != "PRIMARY" && value != "SECONDARY" {
es = append ( es , fmt . Errorf ( "Failover policy type must be PRIMARY or SECONDARY" ) )
}
return
} ,
} ,
} ,
} ,
} ,
2017-03-16 19:57:21 +01:00
"latency_routing_policy" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeList ,
Optional : true ,
ConflictsWith : [ ] string {
"failover_routing_policy" ,
"geolocation_routing_policy" ,
"weighted_routing_policy" ,
} ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
2017-03-16 19:57:21 +01:00
"region" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeString ,
Required : true ,
} ,
} ,
} ,
} ,
2017-03-16 19:57:21 +01:00
"geolocation_routing_policy" : { // AWS Geolocation
2015-08-12 18:03:47 +02:00
Type : schema . TypeList ,
Optional : true ,
ConflictsWith : [ ] string {
"failover_routing_policy" ,
"latency_routing_policy" ,
"weighted_routing_policy" ,
} ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
2017-03-16 19:57:21 +01:00
"continent" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeString ,
Optional : true ,
} ,
2017-03-16 19:57:21 +01:00
"country" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeString ,
Optional : true ,
} ,
2017-03-16 19:57:21 +01:00
"subdivision" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeString ,
Optional : true ,
} ,
} ,
} ,
} ,
2017-03-16 19:57:21 +01:00
"weighted_routing_policy" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeList ,
Optional : true ,
ConflictsWith : [ ] string {
"failover_routing_policy" ,
"geolocation_routing_policy" ,
"latency_routing_policy" ,
} ,
Elem : & schema . Resource {
Schema : map [ string ] * schema . Schema {
2017-03-16 19:57:21 +01:00
"weight" : {
2015-08-12 18:03:47 +02:00
Type : schema . TypeInt ,
Required : true ,
} ,
} ,
} ,
2015-04-28 18:11:38 +02:00
} ,
2017-03-16 19:57:21 +01:00
"health_check_id" : { // ID of health check
2015-04-28 18:11:38 +02:00
Type : schema . TypeString ,
Optional : true ,
} ,
2017-03-16 19:57:21 +01:00
"records" : {
2015-05-01 12:02:06 +02:00
Type : schema . TypeSet ,
ConflictsWith : [ ] string { "alias" } ,
Elem : & schema . Schema { Type : schema . TypeString } ,
Optional : true ,
2015-04-09 15:38:16 +02:00
Set : schema . HashString ,
2014-11-21 17:58:34 +01:00
} ,
2014-07-23 05:08:39 +02:00
} ,
}
}
2015-04-06 17:16:23 +02:00
func resourceAwsRoute53RecordUpdate ( d * schema . ResourceData , meta interface { } ) error {
// Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and
2015-04-06 17:45:02 +02:00
// AWS dynamically determines if a record should be created or updated.
2015-04-06 17:16:23 +02:00
// Amazon Route 53 can update an existing resource record set only when all
2017-01-12 00:08:20 +01:00
// of the following values match: Name, Type and SetIdentifier
// See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html
if ! d . HasChange ( "type" ) && ! d . HasChange ( "set_identifier" ) {
// If neither type nor set_identifier changed we use UPSERT,
// for resouce update here we simply fall through to
// 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 {
2017-03-16 19:57:21 +01:00
{
2017-01-12 00:08:20 +01:00
Action : aws . String ( "DELETE" ) ,
ResourceRecordSet : oldRec ,
} ,
2017-03-16 19:57:21 +01:00
{
2017-01-12 00:08:20 +01:00
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 )
2015-04-06 17:16:23 +02:00
}
2014-11-21 17:58:34 +01:00
func resourceAwsRoute53RecordCreate ( d * schema . ResourceData , meta interface { } ) error {
2015-02-12 17:48:48 +01:00
conn := meta . ( * AWSClient ) . r53conn
2015-04-07 22:20:56 +02:00
zone := cleanZoneID ( d . Get ( "zone_id" ) . ( string ) )
2015-02-12 01:33:21 +01:00
2015-05-20 13:21:23 +02:00
var err error
2015-08-17 20:27:16 +02:00
zoneRecord , err := conn . GetHostedZone ( & route53 . GetHostedZoneInput { Id : aws . String ( zone ) } )
2015-02-12 01:33:21 +01:00
if err != nil {
return err
}
2015-11-16 23:58:11 +01:00
if zoneRecord . HostedZone == nil {
return fmt . Errorf ( "[WARN] No Route53 Zone found for id (%s)" , zone )
}
2015-02-12 01:33:21 +01:00
2016-01-28 23:44:24 +01:00
// Build the record
2015-03-23 21:01:53 +01:00
rec , err := resourceAwsRoute53RecordBuildSet ( d , * zoneRecord . HostedZone . Name )
2014-07-23 05:08:39 +02:00
if err != nil {
2014-11-21 17:58:34 +01:00
return err
2014-07-23 05:08:39 +02:00
}
2014-08-22 21:51:11 +02:00
// Create the new records. We abuse StateChangeConf for this to
// retry for us since Route53 sometimes returns errors about another
// operation happening at the same time.
2015-02-12 17:48:48 +01:00
changeBatch := & route53 . ChangeBatch {
Comment : aws . String ( "Managed by Terraform" ) ,
2015-04-16 20:42:16 +02:00
Changes : [ ] * route53 . Change {
2017-03-16 19:57:21 +01:00
{
2015-02-12 17:48:48 +01:00
Action : aws . String ( "UPSERT" ) ,
ResourceRecordSet : rec ,
2014-07-23 05:08:39 +02:00
} ,
} ,
}
2015-02-12 01:33:21 +01:00
2015-04-16 20:42:16 +02:00
req := & route53 . ChangeResourceRecordSetsInput {
2015-08-17 20:27:16 +02:00
HostedZoneId : aws . String ( cleanZoneID ( * zoneRecord . HostedZone . Id ) ) ,
2015-02-12 17:48:48 +01:00
ChangeBatch : changeBatch ,
}
2015-12-22 23:23:08 +01:00
log . Printf ( "[DEBUG] Creating resource records for zone: %s, name: %s\n\n%s" ,
zone , * rec . Name , req )
2014-11-21 17:58:34 +01:00
2016-08-16 20:34:45 +02:00
respRaw , err := changeRoute53RecordSet ( conn , req )
2016-08-23 19:25:09 +02:00
if err != nil {
return errwrap . Wrapf ( "[ERR]: Error building changeset: {{err}}" , err )
}
2016-08-16 20:34:45 +02:00
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 changeRoute53RecordSet ( conn * route53 . Route53 , input * route53 . ChangeResourceRecordSetsInput ) ( interface { } , error ) {
2014-08-22 21:51:11 +02:00
wait := resource . StateChangeConf {
Pending : [ ] string { "rejected" } ,
2016-01-21 02:20:41 +01:00
Target : [ ] string { "accepted" } ,
2014-08-22 21:51:11 +02:00
Timeout : 5 * time . Minute ,
MinTimeout : 1 * time . Second ,
Refresh : func ( ) ( interface { } , string , error ) {
2016-08-16 20:34:45 +02:00
resp , err := conn . ChangeResourceRecordSets ( input )
2014-08-22 21:51:11 +02:00
if err != nil {
2015-05-20 13:21:23 +02:00
if r53err , ok := err . ( awserr . Error ) ; ok {
if r53err . Code ( ) == "PriorRequestNotComplete" {
2015-03-23 21:01:53 +01:00
// There is some pending operation, so just retry
// in a bit.
return nil , "rejected" , nil
}
2014-08-22 21:51:11 +02:00
}
return nil , "failure" , err
}
2015-02-12 17:48:48 +01:00
return resp , "accepted" , nil
2014-08-22 21:51:11 +02:00
} ,
}
2014-11-21 17:58:34 +01:00
2016-08-16 20:34:45 +02:00
return wait . WaitForState ( )
}
2014-07-23 05:08:39 +02:00
2016-08-16 20:34:45 +02:00
func waitForRoute53RecordSetToSync ( conn * route53 . Route53 , requestId string ) error {
wait := resource . StateChangeConf {
2014-07-23 05:08:39 +02:00
Delay : 30 * time . Second ,
Pending : [ ] string { "PENDING" } ,
2016-01-21 02:20:41 +01:00
Target : [ ] string { "INSYNC" } ,
2015-03-09 20:06:27 +01:00
Timeout : 30 * time . Minute ,
2014-07-23 05:08:39 +02:00
MinTimeout : 5 * time . Second ,
Refresh : func ( ) ( result interface { } , state string , err error ) {
2015-04-16 20:42:16 +02:00
changeRequest := & route53 . GetChangeInput {
2016-08-16 20:34:45 +02:00
Id : aws . String ( requestId ) ,
2015-02-12 17:48:48 +01:00
}
return resourceAwsGoRoute53Wait ( conn , changeRequest )
2014-07-23 05:08:39 +02:00
} ,
}
2016-08-16 20:34:45 +02:00
_ , err := wait . WaitForState ( )
return err
2014-07-23 05:08:39 +02:00
}
2014-11-21 17:58:34 +01:00
func resourceAwsRoute53RecordRead ( d * schema . ResourceData , meta interface { } ) error {
2016-04-21 22:46:37 +02:00
// If we don't have a zone ID we're doing an import. Parse it from the ID.
if _ , ok := d . GetOk ( "zone_id" ) ; ! ok {
parts := strings . Split ( d . Id ( ) , "_" )
2017-02-01 15:15:08 +01:00
2017-05-04 20:51:10 +02:00
//we check that we have parsed the id into the correct number of segments
//we need at least 3 segments!
if len ( parts ) == 1 || len ( parts ) < 3 {
2017-02-01 15:15:08 +01:00
return fmt . Errorf ( "Error Importing aws_route_53 record. Please make sure the record ID is in the form ZONEID_RECORDNAME_TYPE (i.e. Z4KAPRWWNC7JR_dev_A" )
}
2016-04-21 22:46:37 +02:00
d . Set ( "zone_id" , parts [ 0 ] )
d . Set ( "name" , parts [ 1 ] )
d . Set ( "type" , parts [ 2 ] )
if len ( parts ) > 3 {
d . Set ( "set_identifier" , parts [ 3 ] )
}
}
2016-01-28 23:44:24 +01:00
record , err := findRecord ( d , meta )
if err != nil {
2016-01-29 18:56:19 +01:00
switch err {
case r53NoHostedZoneFound , r53NoRecordsFound :
log . Printf ( "[DEBUG] %s for: %s, removing from state file" , err , d . Id ( ) )
d . SetId ( "" )
return nil
default :
return err
}
2016-01-28 23:44:24 +01:00
}
2017-05-02 11:31:51 +02:00
err = d . Set ( "records" , flattenResourceRecords ( record . ResourceRecords , * record . Type ) )
2016-01-28 23:44:24 +01:00
if err != nil {
return fmt . Errorf ( "[DEBUG] Error setting records for: %s, error: %#v" , d . Id ( ) , err )
}
2016-04-21 22:46:37 +02:00
if alias := record . AliasTarget ; alias != nil {
2016-10-31 20:18:00 +01:00
name := normalizeAwsAliasName ( * alias . DNSName )
provider/aws: aws_route53_record alias refresh manually updated record
Fixes #9108
When an aws_route53_record alias is created with terraform and then
modified via cli or console, terraform wasn't picking up the changes. I
had the following config:
```
resource "aws_route53_record" "alias" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "A"
alias {
zone_id = "${aws_elb.main.zone_id}"
name = "${aws_elb.main.dns_name}"
evaluate_target_health = true
}
}
```
I changed the evaluate_health_target on the AWS console and terraform plan showed me this:
```
% terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but
will not be persisted to local or remote state storage.
aws_route53_zone.main: Refreshing state... (ID: Z32Z9B1UPAIP6X)
aws_elb.main: Refreshing state... (ID: foobar-terraform-elb-1111)
aws_route53_record.alias: Refreshing state... (ID: Z32Z9B1UPAIP6X_www_A)
No changes. Infrastructure is up-to-date. This means that Terraform
could not detect any differences between your configuration and
the real physical resources that exist. As a result, Terraform
doesn't need to do anything.
```
When rebuilding the provider with the changes in the PR, a terraform plan then looks as follows:
```
% terraform plan
[WARN] /Users/stacko/Code/go/bin/terraform-provider-aws overrides an internal plugin for aws-provider.
If you did not expect to see this message you will need to remove the old plugin.
See https://www.terraform.io/docs/internals/internal-plugins.html
[WARN] /Users/stacko/Code/go/bin/terraform-provider-azurerm overrides an internal plugin for azurerm-provider.
If you did not expect to see this message you will need to remove the old plugin.
See https://www.terraform.io/docs/internals/internal-plugins.html
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but
will not be persisted to local or remote state storage.
aws_route53_zone.main: Refreshing state... (ID: Z32Z9B1UPAIP6X)
aws_elb.main: Refreshing state... (ID: foobar-terraform-elb-1111)
aws_route53_record.alias: Refreshing state... (ID: Z32Z9B1UPAIP6X_www_A)
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.
Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.
~ aws_route53_record.alias
alias.1050468691.evaluate_target_health: "" => "true"
alias.1050468691.name: "" => "foobar-terraform-elb-1111-522021794.us-west-2.elb.amazonaws.com"
alias.1050468691.zone_id: "" => "Z1H1FL5HABSF5"
alias.2906616344.evaluate_target_health: "false" => "false"
alias.2906616344.name: "foobar-terraform-elb-1111-522021794.us-west-2.elb.amazonaws.com." => ""
alias.2906616344.zone_id: "Z1H1FL5HABSF5" => ""
Plan: 0 to add, 1 to change, 0 to destroy.
```
the apply then changed the target back to true
```
% make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSRoute53Record_'
==> Checking that code complies with gofmt requirements...
go generate $(go list ./... | grep -v /terraform/vendor/)
2016/09/29 18:17:23 Generated command/internal_plugin_list.go
TF_ACC=1 go test ./builtin/providers/aws -v
-run=TestAccAWSRoute53Record_ -timeout 120m
=== RUN TestAccAWSRoute53Record_basic
--- PASS: TestAccAWSRoute53Record_basic (120.63s)
=== RUN TestAccAWSRoute53Record_basic_fqdn
--- PASS: TestAccAWSRoute53Record_basic_fqdn (131.81s)
=== RUN TestAccAWSRoute53Record_txtSupport
--- PASS: TestAccAWSRoute53Record_txtSupport (128.40s)
=== RUN TestAccAWSRoute53Record_spfSupport
--- PASS: TestAccAWSRoute53Record_spfSupport (120.06s)
=== RUN TestAccAWSRoute53Record_generatesSuffix
--- PASS: TestAccAWSRoute53Record_generatesSuffix (114.02s)
=== RUN TestAccAWSRoute53Record_wildcard
--- PASS: TestAccAWSRoute53Record_wildcard (165.54s)
=== RUN TestAccAWSRoute53Record_failover
--- PASS: TestAccAWSRoute53Record_failover (118.10s)
=== RUN TestAccAWSRoute53Record_weighted_basic
--- PASS: TestAccAWSRoute53Record_weighted_basic (128.10s)
=== RUN TestAccAWSRoute53Record_alias
--- PASS: TestAccAWSRoute53Record_alias (132.62s)
=== RUN TestAccAWSRoute53Record_s3_alias
--- PASS: TestAccAWSRoute53Record_s3_alias (132.12s)
=== RUN TestAccAWSRoute53Record_weighted_alias
--- PASS: TestAccAWSRoute53Record_weighted_alias (237.92s)
=== RUN TestAccAWSRoute53Record_geolocation_basic
--- PASS: TestAccAWSRoute53Record_geolocation_basic (121.95s)
=== RUN TestAccAWSRoute53Record_latency_basic
--- PASS: TestAccAWSRoute53Record_latency_basic (123.40s)
=== RUN TestAccAWSRoute53Record_TypeChange
--- PASS: TestAccAWSRoute53Record_TypeChange (198.24s)
=== RUN TestAccAWSRoute53Record_empty
--- PASS: TestAccAWSRoute53Record_empty (119.68s)
PASS
ok github.com/hashicorp/terraform/builtin/providers/aws2092.597s
```
2016-09-29 15:17:09 +02:00
d . Set ( "alias" , [ ] interface { } {
map [ string ] interface { } {
"zone_id" : * alias . HostedZoneId ,
"name" : name ,
"evaluate_target_health" : * alias . EvaluateTargetHealth ,
} ,
} )
2016-04-21 22:46:37 +02:00
}
2016-01-28 23:44:24 +01:00
d . Set ( "ttl" , record . TTL )
2015-08-12 18:03:47 +02:00
if record . Failover != nil {
v := [ ] map [ string ] interface { } { {
"type" : aws . StringValue ( record . Failover ) ,
} }
2016-05-31 22:24:46 +02:00
if err := d . Set ( "failover_routing_policy" , v ) ; err != nil {
return fmt . Errorf ( "[DEBUG] Error setting failover records for: %s, error: %#v" , d . Id ( ) , err )
2015-08-12 18:03:47 +02:00
}
}
if record . GeoLocation != nil {
v := [ ] map [ string ] interface { } { {
"continent" : aws . StringValue ( record . GeoLocation . ContinentCode ) ,
"country" : aws . StringValue ( record . GeoLocation . CountryCode ) ,
"subdivision" : aws . StringValue ( record . GeoLocation . SubdivisionCode ) ,
} }
2016-05-31 22:24:46 +02:00
if err := d . Set ( "geolocation_routing_policy" , v ) ; err != nil {
return fmt . Errorf ( "[DEBUG] Error setting gelocation records for: %s, error: %#v" , d . Id ( ) , err )
2015-08-12 18:03:47 +02:00
}
}
if record . Region != nil {
v := [ ] map [ string ] interface { } { {
"region" : aws . StringValue ( record . Region ) ,
} }
2016-05-31 22:24:46 +02:00
if err := d . Set ( "latency_routing_policy" , v ) ; err != nil {
return fmt . Errorf ( "[DEBUG] Error setting latency records for: %s, error: %#v" , d . Id ( ) , err )
2015-08-12 18:03:47 +02:00
}
}
2016-01-28 23:44:24 +01:00
if record . Weight != nil {
2015-08-12 18:03:47 +02:00
v := [ ] map [ string ] interface { } { {
"weight" : aws . Int64Value ( ( record . Weight ) ) ,
} }
2016-05-31 22:24:46 +02:00
if err := d . Set ( "weighted_routing_policy" , v ) ; err != nil {
return fmt . Errorf ( "[DEBUG] Error setting weighted records for: %s, error: %#v" , d . Id ( ) , err )
2015-08-12 18:03:47 +02:00
}
2016-01-28 23:44:24 +01:00
}
2015-08-12 18:03:47 +02:00
2016-01-28 23:44:24 +01:00
d . Set ( "set_identifier" , record . SetIdentifier )
d . Set ( "health_check_id" , record . HealthCheckId )
return nil
}
2014-11-21 17:58:34 +01:00
2016-01-29 18:56:19 +01:00
// findRecord takes a ResourceData struct for aws_resource_route53_record. It
// uses the referenced zone_id to query Route53 and find information on it's
// records.
//
// If records are found, it returns the matching
// route53.ResourceRecordSet and nil for the error.
//
// If no hosted zone is found, it returns a nil recordset and r53NoHostedZoneFound
// error.
//
// If no matching recordset is found, it returns nil and a r53NoRecordsFound
// error
//
// If there are other errors, it returns nil a nil recordset and passes on the
// error.
2016-01-28 23:44:24 +01:00
func findRecord ( d * schema . ResourceData , meta interface { } ) ( * route53 . ResourceRecordSet , error ) {
conn := meta . ( * AWSClient ) . r53conn
// Scan for a
2015-04-07 22:20:56 +02:00
zone := cleanZoneID ( d . Get ( "zone_id" ) . ( string ) )
2015-03-23 21:01:53 +01:00
// get expanded name
2015-08-17 20:27:16 +02:00
zoneRecord , err := conn . GetHostedZone ( & route53 . GetHostedZoneInput { Id : aws . String ( zone ) } )
2015-03-23 21:01:53 +01:00
if err != nil {
2015-12-07 23:33:37 +01:00
if r53err , ok := err . ( awserr . Error ) ; ok && r53err . Code ( ) == "NoSuchHostedZone" {
2016-01-29 18:56:19 +01:00
return nil , r53NoHostedZoneFound
2015-12-07 23:33:37 +01:00
}
2016-01-28 23:44:24 +01:00
return nil , err
2015-03-23 21:01:53 +01:00
}
2016-05-09 23:48:16 +02:00
2015-03-23 21:01:53 +01:00
en := expandRecordName ( d . Get ( "name" ) . ( string ) , * zoneRecord . HostedZone . Name )
2015-05-07 13:54:53 +02:00
log . Printf ( "[DEBUG] Expanded record name: %s" , en )
d . Set ( "fqdn" , en )
2015-03-23 21:01:53 +01:00
2015-04-16 20:42:16 +02:00
lopts := & route53 . ListResourceRecordSetsInput {
2015-08-17 20:27:16 +02:00
HostedZoneId : aws . String ( cleanZoneID ( zone ) ) ,
2015-03-23 21:01:53 +01:00
StartRecordName : aws . String ( en ) ,
2015-02-12 17:48:48 +01:00
StartRecordType : aws . String ( d . Get ( "type" ) . ( string ) ) ,
2014-11-21 17:58:34 +01:00
}
2015-02-12 17:48:48 +01:00
2015-10-20 20:32:35 +02:00
log . Printf ( "[DEBUG] List resource records sets for zone: %s, opts: %s" ,
zone , lopts )
2015-02-12 17:48:48 +01:00
resp , err := conn . ListResourceRecordSets ( lopts )
2014-07-23 05:08:39 +02:00
if err != nil {
2016-01-28 23:44:24 +01:00
return nil , err
2014-07-23 05:08:39 +02:00
}
2015-02-12 17:48:48 +01:00
for _ , record := range resp . ResourceRecordSets {
2015-03-17 20:57:45 +01:00
name := cleanRecordName ( * record . Name )
2015-10-20 20:32:35 +02:00
if FQDN ( strings . ToLower ( name ) ) != FQDN ( strings . ToLower ( * lopts . StartRecordName ) ) {
2014-11-21 17:58:34 +01:00
continue
}
2015-02-12 17:48:48 +01:00
if strings . ToUpper ( * record . Type ) != strings . ToUpper ( * lopts . StartRecordType ) {
2014-11-21 17:58:34 +01:00
continue
}
2015-04-28 18:11:38 +02:00
if record . SetIdentifier != nil && * record . SetIdentifier != d . Get ( "set_identifier" ) {
2015-04-17 19:02:05 +02:00
continue
}
2016-01-29 18:56:19 +01:00
// The only safe return where a record is found
2016-01-28 23:44:24 +01:00
return record , nil
2014-07-23 05:08:39 +02:00
}
2016-01-29 18:56:19 +01:00
return nil , r53NoRecordsFound
2014-07-23 05:08:39 +02:00
}
2014-11-21 17:58:34 +01:00
func resourceAwsRoute53RecordDelete ( d * schema . ResourceData , meta interface { } ) error {
2015-02-12 17:48:48 +01:00
conn := meta . ( * AWSClient ) . r53conn
2014-11-21 17:58:34 +01:00
// Get the records
2016-01-28 23:44:24 +01:00
rec , err := findRecord ( d , meta )
2014-07-23 05:08:39 +02:00
if err != nil {
2016-01-29 19:38:22 +01:00
switch err {
case r53NoHostedZoneFound , r53NoRecordsFound :
log . Printf ( "[DEBUG] %s for: %s, removing from state file" , err , d . Id ( ) )
d . SetId ( "" )
return nil
default :
return err
}
2014-07-23 05:08:39 +02:00
}
2016-02-02 19:37:06 +01:00
// Change batch for deleting
2015-02-12 17:48:48 +01:00
changeBatch := & route53 . ChangeBatch {
Comment : aws . String ( "Deleted by Terraform" ) ,
2015-04-16 20:42:16 +02:00
Changes : [ ] * route53 . Change {
2017-03-16 19:57:21 +01:00
{
2015-02-12 17:48:48 +01:00
Action : aws . String ( "DELETE" ) ,
ResourceRecordSet : rec ,
2014-07-23 05:08:39 +02:00
} ,
} ,
}
2015-02-12 17:48:48 +01:00
2016-01-28 23:44:24 +01:00
zone := cleanZoneID ( d . Get ( "zone_id" ) . ( string ) )
2015-04-16 20:42:16 +02:00
req := & route53 . ChangeResourceRecordSetsInput {
2015-08-17 20:27:16 +02:00
HostedZoneId : aws . String ( cleanZoneID ( zone ) ) ,
2015-02-12 17:48:48 +01:00
ChangeBatch : changeBatch ,
}
2014-11-21 17:58:34 +01:00
2016-09-03 17:14:50 +02:00
respRaw , err := deleteRoute53RecordSet ( conn , req )
if err != nil {
return errwrap . Wrapf ( "[ERR]: Error building changeset: {{err}}" , err )
}
changeInfo := respRaw . ( * route53 . ChangeResourceRecordSetsOutput ) . ChangeInfo
2016-09-26 12:27:56 +02:00
if changeInfo == nil {
log . Printf ( "[INFO] No ChangeInfo Found. Waiting for Sync not required" )
return nil
}
2016-09-03 17:14:50 +02:00
err = waitForRoute53RecordSetToSync ( conn , cleanChangeID ( * changeInfo . Id ) )
if err != nil {
return err
}
2016-08-16 20:34:45 +02:00
return err
}
func deleteRoute53RecordSet ( conn * route53 . Route53 , input * route53 . ChangeResourceRecordSetsInput ) ( interface { } , error ) {
2014-08-31 18:12:05 +02:00
wait := resource . StateChangeConf {
Pending : [ ] string { "rejected" } ,
2016-01-21 02:20:41 +01:00
Target : [ ] string { "accepted" } ,
2014-08-31 18:12:05 +02:00
Timeout : 5 * time . Minute ,
MinTimeout : 1 * time . Second ,
Refresh : func ( ) ( interface { } , string , error ) {
2016-08-16 20:34:45 +02:00
resp , err := conn . ChangeResourceRecordSets ( input )
2014-08-31 18:12:05 +02:00
if err != nil {
2015-05-20 13:21:23 +02:00
if r53err , ok := err . ( awserr . Error ) ; ok {
if r53err . Code ( ) == "PriorRequestNotComplete" {
2015-03-17 20:57:45 +01:00
// There is some pending operation, so just retry
// in a bit.
return 42 , "rejected" , nil
}
2015-05-20 13:21:23 +02:00
if r53err . Code ( ) == "InvalidChangeBatch" {
2015-03-17 20:57:45 +01:00
// This means that the record is already gone.
2016-08-16 20:34:45 +02:00
return resp , "accepted" , nil
2015-03-17 20:57:45 +01:00
}
2014-10-08 06:55:40 +02:00
}
return 42 , "failure" , err
2014-08-31 18:12:05 +02:00
}
2016-08-16 20:34:45 +02:00
return resp , "accepted" , nil
2014-08-31 18:12:05 +02:00
} ,
}
2014-11-21 17:58:34 +01:00
2016-08-16 20:34:45 +02:00
return wait . WaitForState ( )
2014-07-23 05:08:39 +02:00
}
2015-03-23 21:01:53 +01:00
func resourceAwsRoute53RecordBuildSet ( d * schema . ResourceData , zoneName string ) ( * route53 . ResourceRecordSet , error ) {
// get expanded name
en := expandRecordName ( d . Get ( "name" ) . ( string ) , zoneName )
// Create the RecordSet request with the fully expanded name, e.g.
// sub.domain.com. Route 53 requires a fully qualified domain name, but does
// not require the trailing ".", which it will itself, so we don't call FQDN
// here.
2014-11-21 17:58:34 +01:00
rec := & route53 . ResourceRecordSet {
2015-05-01 12:02:06 +02:00
Name : aws . String ( en ) ,
Type : aws . String ( d . Get ( "type" ) . ( string ) ) ,
}
if v , ok := d . GetOk ( "ttl" ) ; ok {
2015-07-28 22:29:46 +02:00
rec . TTL = aws . Int64 ( int64 ( v . ( int ) ) )
2015-05-01 12:02:06 +02:00
}
// Resource records
if v , ok := d . GetOk ( "records" ) ; ok {
recs := v . ( * schema . Set ) . List ( )
rec . ResourceRecords = expandResourceRecords ( recs , d . Get ( "type" ) . ( string ) )
}
// Alias record
if v , ok := d . GetOk ( "alias" ) ; ok {
aliases := v . ( * schema . Set ) . List ( )
if len ( aliases ) > 1 {
return nil , fmt . Errorf ( "You can only define a single alias target per record" )
}
alias := aliases [ 0 ] . ( map [ string ] interface { } )
rec . AliasTarget = & route53 . AliasTarget {
DNSName : aws . String ( alias [ "name" ] . ( string ) ) ,
2015-07-28 22:29:46 +02:00
EvaluateTargetHealth : aws . Bool ( alias [ "evaluate_target_health" ] . ( bool ) ) ,
2015-08-17 20:27:16 +02:00
HostedZoneId : aws . String ( alias [ "zone_id" ] . ( string ) ) ,
2015-05-01 12:02:06 +02:00
}
log . Printf ( "[DEBUG] Creating alias: %#v" , alias )
2015-05-27 01:27:18 +02:00
} else {
if _ , ok := d . GetOk ( "ttl" ) ; ! ok {
return nil , fmt . Errorf ( ` provider.aws: aws_route53_record: %s: "ttl": required field is not set ` , d . Get ( "name" ) . ( string ) )
}
if _ , ok := d . GetOk ( "records" ) ; ! ok {
return nil , fmt . Errorf ( ` provider.aws: aws_route53_record: %s: "records": required field is not set ` , d . Get ( "name" ) . ( string ) )
}
2014-07-23 05:08:39 +02:00
}
2015-04-17 19:02:05 +02:00
2015-08-12 18:03:47 +02:00
if v , ok := d . GetOk ( "failover_routing_policy" ) ; ok {
2016-03-22 14:19:24 +01:00
if _ , ok := d . GetOk ( "set_identifier" ) ; ! ok {
2015-08-12 18:03:47 +02:00
return nil , fmt . Errorf ( ` provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "failover_routing_policy" is set ` , d . Get ( "name" ) . ( string ) )
}
records := v . ( [ ] interface { } )
if len ( records ) > 1 {
return nil , fmt . Errorf ( "You can only define a single failover_routing_policy per record" )
2016-03-22 14:19:24 +01:00
}
2015-08-12 18:03:47 +02:00
failover := records [ 0 ] . ( map [ string ] interface { } )
rec . Failover = aws . String ( failover [ "type" ] . ( string ) )
2015-04-28 18:11:38 +02:00
}
if v , ok := d . GetOk ( "health_check_id" ) ; ok {
2015-08-17 20:27:16 +02:00
rec . HealthCheckId = aws . String ( v . ( string ) )
2015-04-28 18:11:38 +02:00
}
2015-08-12 18:03:47 +02:00
if v , ok := d . GetOk ( "weighted_routing_policy" ) ; ok {
if _ , ok := d . GetOk ( "set_identifier" ) ; ! ok {
return nil , fmt . Errorf ( ` provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "weight_routing_policy" is set ` , d . Get ( "name" ) . ( string ) )
}
records := v . ( [ ] interface { } )
if len ( records ) > 1 {
return nil , fmt . Errorf ( "You can only define a single weighed_routing_policy per record" )
}
weight := records [ 0 ] . ( map [ string ] interface { } )
rec . Weight = aws . Int64 ( int64 ( weight [ "weight" ] . ( int ) ) )
}
2015-04-17 19:02:05 +02:00
if v , ok := d . GetOk ( "set_identifier" ) ; ok {
rec . SetIdentifier = aws . String ( v . ( string ) )
2015-11-12 21:57:41 +01:00
}
2015-08-12 18:03:47 +02:00
if v , ok := d . GetOk ( "latency_routing_policy" ) ; ok {
2016-03-22 13:20:47 +01:00
if _ , ok := d . GetOk ( "set_identifier" ) ; ! ok {
2015-08-12 18:03:47 +02:00
return nil , fmt . Errorf ( ` provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "latency_routing_policy" is set ` , d . Get ( "name" ) . ( string ) )
}
records := v . ( [ ] interface { } )
if len ( records ) > 1 {
return nil , fmt . Errorf ( "You can only define a single latency_routing_policy per record" )
2016-03-22 13:20:47 +01:00
}
2015-08-12 18:03:47 +02:00
latency := records [ 0 ] . ( map [ string ] interface { } )
rec . Region = aws . String ( latency [ "region" ] . ( string ) )
}
if v , ok := d . GetOk ( "geolocation_routing_policy" ) ; ok {
if _ , ok := d . GetOk ( "set_identifier" ) ; ! ok {
return nil , fmt . Errorf ( ` provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "geolocation_routing_policy" is set ` , d . Get ( "name" ) . ( string ) )
}
geolocations := v . ( [ ] interface { } )
if len ( geolocations ) > 1 {
return nil , fmt . Errorf ( "You can only define a single geolocation_routing_policy per record" )
}
geolocation := geolocations [ 0 ] . ( map [ string ] interface { } )
rec . GeoLocation = & route53 . GeoLocation {
ContinentCode : nilString ( geolocation [ "continent" ] . ( string ) ) ,
CountryCode : nilString ( geolocation [ "country" ] . ( string ) ) ,
SubdivisionCode : nilString ( geolocation [ "subdivision" ] . ( string ) ) ,
}
log . Printf ( "[DEBUG] Creating geolocation: %#v" , geolocation )
2015-04-17 19:02:05 +02:00
}
2014-11-21 17:58:34 +01:00
return rec , nil
2014-07-23 05:08:39 +02:00
}
2015-02-12 17:48:48 +01:00
func FQDN ( name string ) string {
n := len ( name )
if n == 0 || name [ n - 1 ] == '.' {
return name
} else {
return name + "."
}
}
2015-03-17 20:57:45 +01:00
// Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the
// octal equivalent, "\\052". Here we look for that, and convert back to "*"
// as needed.
func cleanRecordName ( name string ) string {
str := name
if strings . HasPrefix ( name , "\\052" ) {
str = strings . Replace ( name , "\\052" , "*" , 1 )
log . Printf ( "[DEBUG] Replacing octal \\052 for * in: %s" , name )
}
return str
}
2015-03-23 21:01:53 +01:00
// Check if the current record name contains the zone suffix.
// If it does not, add the zone name to form a fully qualified name
// and keep AWS happy.
func expandRecordName ( name , zone string ) string {
2015-10-20 23:36:25 +02:00
rn := strings . ToLower ( strings . TrimSuffix ( name , "." ) )
2015-03-23 21:01:53 +01:00
zone = strings . TrimSuffix ( zone , "." )
if ! strings . HasSuffix ( rn , zone ) {
2016-07-07 16:01:33 +02:00
if len ( name ) == 0 {
rn = zone
} else {
rn = strings . Join ( [ ] string { name , zone } , "." )
}
2015-03-23 21:01:53 +01:00
}
return rn
}
2015-05-01 12:02:06 +02:00
func resourceAwsRoute53AliasRecordHash ( v interface { } ) int {
var buf bytes . Buffer
m := v . ( map [ string ] interface { } )
2016-10-31 20:18:00 +01:00
buf . WriteString ( fmt . Sprintf ( "%s-" , normalizeAwsAliasName ( m [ "name" ] . ( string ) ) ) )
2015-05-01 12:02:06 +02:00
buf . WriteString ( fmt . Sprintf ( "%s-" , m [ "zone_id" ] . ( string ) ) )
buf . WriteString ( fmt . Sprintf ( "%t-" , m [ "evaluate_target_health" ] . ( bool ) ) )
return hashcode . String ( buf . String ( ) )
}
2015-08-12 18:03:47 +02:00
// nilString takes a string as an argument and returns a string
// pointer. The returned pointer is nil if the string argument is
// empty, otherwise it is a pointer to a copy of the string.
func nilString ( s string ) * string {
if s == "" {
return nil
}
return aws . String ( s )
}
2016-10-31 20:18:00 +01:00
func normalizeAwsAliasName ( alias interface { } ) string {
input := alias . ( string )
if strings . HasPrefix ( input , "dualstack." ) {
return strings . Replace ( input , "dualstack." , "" , - 1 )
}
return strings . TrimRight ( input , "." )
}