diff --git a/.changelog/23602.txt b/.changelog/23602.txt new file mode 100644 index 000000000000..2a60408c7672 --- /dev/null +++ b/.changelog/23602.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_route53_traffic_policy +``` + +```release-note:new-data-source +aws_route53_traffic_policy_document +``` + +```release-note:new-resource +aws_route53_traffic_policy_instance +``` \ No newline at end of file diff --git a/internal/generate/listpages/README.md b/internal/generate/listpages/README.md index 3e2f6f142097..521047d81826 100644 --- a/internal/generate/listpages/README.md +++ b/internal/generate/listpages/README.md @@ -7,10 +7,11 @@ For example, the EC2 API defines both [`DescribeInstancesPages`](https://docs.aw The `listpages` executable is called as follows: ```console -$ go run main.go -ListOps [,] +$ go run main.go -ListOps [,] [] ``` * ``: Name of a function to wrap +* ``: Name of the generated lister source file, defaults to `list_pages_gen.go` Optional Flags: diff --git a/internal/generate/listpages/main.go b/internal/generate/listpages/main.go index 5765f65a0704..4f9ce2ef8759 100644 --- a/internal/generate/listpages/main.go +++ b/internal/generate/listpages/main.go @@ -20,7 +20,7 @@ import ( ) const ( - filename = "list_pages_gen.go" + defaultFilename = "list_pages_gen.go" ) var ( @@ -31,7 +31,7 @@ var ( func usage() { fmt.Fprintf(os.Stderr, "Usage:\n") - fmt.Fprintf(os.Stderr, "\tmain.go [flags]\n\n") + fmt.Fprintf(os.Stderr, "\tmain.go [flags] []\n\n") fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() } @@ -49,6 +49,11 @@ func main() { flag.Usage = usage flag.Parse() + filename := defaultFilename + if args := flag.Args(); len(args) > 0 { + filename = args[0] + } + wd, err := os.Getwd() if err != nil { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 740144a03e3f..26d625845c95 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -754,8 +754,9 @@ func Provider() *schema.Provider { "aws_resourcegroupstaggingapi_resources": resourcegroupstaggingapi.DataSourceResources(), - "aws_route53_delegation_set": route53.DataSourceDelegationSet(), - "aws_route53_zone": route53.DataSourceZone(), + "aws_route53_delegation_set": route53.DataSourceDelegationSet(), + "aws_route53_traffic_policy_document": route53.DataSourceTrafficPolicyDocument(), + "aws_route53_zone": route53.DataSourceZone(), "aws_route53_resolver_endpoint": route53resolver.DataSourceEndpoint(), "aws_route53_resolver_rule": route53resolver.DataSourceRule(), @@ -1669,6 +1670,8 @@ func Provider() *schema.Provider { "aws_route53_key_signing_key": route53.ResourceKeySigningKey(), "aws_route53_query_log": route53.ResourceQueryLog(), "aws_route53_record": route53.ResourceRecord(), + "aws_route53_traffic_policy": route53.ResourceTrafficPolicy(), + "aws_route53_traffic_policy_instance": route53.ResourceTrafficPolicyInstance(), "aws_route53_vpc_association_authorization": route53.ResourceVPCAssociationAuthorization(), "aws_route53_zone": route53.ResourceZone(), "aws_route53_zone_association": route53.ResourceZoneAssociation(), diff --git a/internal/service/route53/enum.go b/internal/service/route53/enum.go index bb8204082e5f..be2611b7bbb6 100644 --- a/internal/service/route53/enum.go +++ b/internal/service/route53/enum.go @@ -12,4 +12,10 @@ const ( ServeSignatureInternalFailure = "INTERNAL_FAILURE" ServeSignatureNotSigning = "NOT_SIGNING" ServeSignatureSigning = "SIGNING" + + TrafficPolicyInstanceStateApplied = "Applied" + TrafficPolicyInstanceStateCreating = "Creating" + TrafficPolicyInstanceStateDeleting = "Deleting" + TrafficPolicyInstanceStateFailed = "Failed" + TrafficPolicyInstanceStateUpdating = "Updating" ) diff --git a/internal/service/route53/find.go b/internal/service/route53/find.go index 3f3f3693d6c2..c5f3dc98b69a 100644 --- a/internal/service/route53/find.go +++ b/internal/service/route53/find.go @@ -1,6 +1,7 @@ package route53 import ( + "context" "fmt" "github.com/aws/aws-sdk-go/aws" @@ -85,3 +86,80 @@ func FindKeySigningKeyByResourceID(conn *route53.Route53, resourceID string) (*r return FindKeySigningKey(conn, hostedZoneID, name) } + +func FindTrafficPolicyByID(ctx context.Context, conn *route53.Route53, id string) (*route53.TrafficPolicy, error) { + var latestVersion int64 + + err := listTrafficPoliciesPagesWithContext(ctx, conn, &route53.ListTrafficPoliciesInput{}, func(page *route53.ListTrafficPoliciesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.TrafficPolicySummaries { + if aws.StringValue(v.Id) == id { + latestVersion = aws.Int64Value(v.LatestVersion) + + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if latestVersion == 0 { + return nil, tfresource.NewEmptyResultError(id) + } + + input := &route53.GetTrafficPolicyInput{ + Id: aws.String(id), + Version: aws.Int64(latestVersion), + } + + output, err := conn.GetTrafficPolicyWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchTrafficPolicy) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.TrafficPolicy == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.TrafficPolicy, nil +} + +func FindTrafficPolicyInstanceByID(ctx context.Context, conn *route53.Route53, id string) (*route53.TrafficPolicyInstance, error) { + input := &route53.GetTrafficPolicyInstanceInput{ + Id: aws.String(id), + } + + output, err := conn.GetTrafficPolicyInstanceWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchTrafficPolicyInstance) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.TrafficPolicyInstance == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.TrafficPolicyInstance, nil +} diff --git a/internal/service/route53/generate.go b/internal/service/route53/generate.go index b7f24e00eb57..01bffe293cf5 100644 --- a/internal/service/route53/generate.go +++ b/internal/service/route53/generate.go @@ -1,3 +1,5 @@ +//go:generate go run ../../generate/listpages/main.go -ListOps=ListTrafficPolicies -Paginator=TrafficPolicyIdMarker list_traffic_policies_pages_gen.go +//go:generate go run ../../generate/listpages/main.go -ListOps=ListTrafficPolicyVersions -Paginator=TrafficPolicyVersionMarker list_traffic_policy_versions_pages_gen.go //go:generate go run ../../generate/tags/main.go -ListTags -ListTagsInIDElem=ResourceId -ListTagsOutTagsElem=ResourceTagSet.Tags -ServiceTagsSlice -TagOp=ChangeTagsForResource -TagInIDElem=ResourceId -TagInTagsElem=AddTags -TagResTypeElem=ResourceType -UntagOp=ChangeTagsForResource -UntagInTagsElem=RemoveTagKeys -UpdateTags // ONLY generate directives and package declaration! Do not add anything else to this file. diff --git a/internal/service/route53/list_pages.go b/internal/service/route53/list_pages.go new file mode 100644 index 000000000000..2d559bf9a378 --- /dev/null +++ b/internal/service/route53/list_pages.go @@ -0,0 +1,36 @@ +//go:build !generate +// +build !generate + +package route53 + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" +) + +// Custom Route 53 service lister functions using the same format as generated code. + +func listTrafficPolicyInstancesPages(conn *route53.Route53, input *route53.ListTrafficPolicyInstancesInput, fn func(*route53.ListTrafficPolicyInstancesOutput, bool) bool) error { //nolint:deadcode // This function is called from a sweeper. + return listTrafficPolicyInstancesPagesWithContext(context.Background(), conn, input, fn) +} + +func listTrafficPolicyInstancesPagesWithContext(ctx context.Context, conn *route53.Route53, input *route53.ListTrafficPolicyInstancesInput, fn func(*route53.ListTrafficPolicyInstancesOutput, bool) bool) error { + for { + output, err := conn.ListTrafficPolicyInstancesWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := !aws.BoolValue(output.IsTruncated) + if !fn(output, lastPage) || lastPage { + break + } + + input.HostedZoneIdMarker = output.HostedZoneIdMarker + input.TrafficPolicyInstanceNameMarker = output.TrafficPolicyInstanceNameMarker + input.TrafficPolicyInstanceTypeMarker = output.TrafficPolicyInstanceTypeMarker + } + return nil +} diff --git a/internal/service/route53/list_traffic_policies_pages_gen.go b/internal/service/route53/list_traffic_policies_pages_gen.go new file mode 100644 index 000000000000..ad403e3b140b --- /dev/null +++ b/internal/service/route53/list_traffic_policies_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "internal/generate/listpages/main.go -ListOps=ListTrafficPolicies -Paginator=TrafficPolicyIdMarker list_traffic_policies_pages_gen.go"; DO NOT EDIT. + +package route53 + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" +) + +func listTrafficPoliciesPages(conn *route53.Route53, input *route53.ListTrafficPoliciesInput, fn func(*route53.ListTrafficPoliciesOutput, bool) bool) error { + return listTrafficPoliciesPagesWithContext(context.Background(), conn, input, fn) +} + +func listTrafficPoliciesPagesWithContext(ctx context.Context, conn *route53.Route53, input *route53.ListTrafficPoliciesInput, fn func(*route53.ListTrafficPoliciesOutput, bool) bool) error { + for { + output, err := conn.ListTrafficPoliciesWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.TrafficPolicyIdMarker) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.TrafficPolicyIdMarker = output.TrafficPolicyIdMarker + } + return nil +} diff --git a/internal/service/route53/list_traffic_policy_versions_pages_gen.go b/internal/service/route53/list_traffic_policy_versions_pages_gen.go new file mode 100644 index 000000000000..763250c4f5df --- /dev/null +++ b/internal/service/route53/list_traffic_policy_versions_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "internal/generate/listpages/main.go -ListOps=ListTrafficPolicyVersions -Paginator=TrafficPolicyVersionMarker list_traffic_policy_versions_pages_gen.go"; DO NOT EDIT. + +package route53 + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" +) + +func listTrafficPolicyVersionsPages(conn *route53.Route53, input *route53.ListTrafficPolicyVersionsInput, fn func(*route53.ListTrafficPolicyVersionsOutput, bool) bool) error { + return listTrafficPolicyVersionsPagesWithContext(context.Background(), conn, input, fn) +} + +func listTrafficPolicyVersionsPagesWithContext(ctx context.Context, conn *route53.Route53, input *route53.ListTrafficPolicyVersionsInput, fn func(*route53.ListTrafficPolicyVersionsOutput, bool) bool) error { + for { + output, err := conn.ListTrafficPolicyVersionsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.TrafficPolicyVersionMarker) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.TrafficPolicyVersionMarker = output.TrafficPolicyVersionMarker + } + return nil +} diff --git a/internal/service/route53/status.go b/internal/service/route53/status.go index c007e0752cdd..c6d8bc5bda76 100644 --- a/internal/service/route53/status.go +++ b/internal/service/route53/status.go @@ -1,9 +1,12 @@ package route53 import ( + "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func statusChangeInfo(conn *route53.Route53, changeID string) resource.StateRefreshFunc { @@ -57,3 +60,19 @@ func statusKeySigningKey(conn *route53.Route53, hostedZoneID string, name string return keySigningKey, aws.StringValue(keySigningKey.Status), nil } } + +func statusTrafficPolicyInstanceState(ctx context.Context, conn *route53.Route53, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindTrafficPolicyInstanceByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.State), nil + } +} diff --git a/internal/service/route53/sweep.go b/internal/service/route53/sweep.go index faea7a5b14e4..77c898289e0d 100644 --- a/internal/service/route53/sweep.go +++ b/internal/service/route53/sweep.go @@ -22,7 +22,7 @@ import ( func init() { resource.AddTestSweepers("aws_route53_health_check", &resource.Sweeper{ Name: "aws_route53_health_check", - F: sweepHealthchecks, + F: sweepHealthChecks, }) resource.AddTestSweepers("aws_route53_key_signing_key", &resource.Sweeper{ @@ -35,6 +35,19 @@ func init() { F: sweepQueryLogs, }) + resource.AddTestSweepers("aws_route53_traffic_policy", &resource.Sweeper{ + Name: "aws_route53_traffic_policy", + F: sweepTrafficPolicies, + Dependencies: []string{ + "aws_route53_traffic_policy_instance", + }, + }) + + resource.AddTestSweepers("aws_route53_traffic_policy_instance", &resource.Sweeper{ + Name: "aws_route53_traffic_policy_instance", + F: sweepTrafficPolicyInstances, + }) + resource.AddTestSweepers("aws_route53_zone", &resource.Sweeper{ Name: "aws_route53_zone", Dependencies: []string{ @@ -43,12 +56,13 @@ func init() { "aws_service_discovery_private_dns_namespace", "aws_elb", "aws_route53_key_signing_key", + "aws_route53_traffic_policy", }, F: sweepZones, }) } -func sweepHealthchecks(region string) error { +func sweepHealthChecks(region string) error { client, err := sweep.SharedRegionalSweepClient(region) if err != nil { @@ -221,6 +235,92 @@ func sweepQueryLogs(region string) error { return sweeperErrs.ErrorOrNil() } +func sweepTrafficPolicies(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*conns.AWSClient).Route53Conn + input := &route53.ListTrafficPoliciesInput{} + sweepResources := make([]*sweep.SweepResource, 0) + + err = listTrafficPoliciesPages(conn, input, func(page *route53.ListTrafficPoliciesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.TrafficPolicySummaries { + r := ResourceTrafficPolicy() + d := r.Data(nil) + d.SetId(aws.StringValue(v.Id)) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping Route 53 Traffic Policy sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Route 53 Traffic Policies (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping Route 53 Traffic Policies (%s): %w", region, err) + } + + return nil +} + +func sweepTrafficPolicyInstances(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*conns.AWSClient).Route53Conn + input := &route53.ListTrafficPolicyInstancesInput{} + sweepResources := make([]*sweep.SweepResource, 0) + + err = listTrafficPolicyInstancesPages(conn, input, func(page *route53.ListTrafficPolicyInstancesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.TrafficPolicyInstances { + r := ResourceTrafficPolicyInstance() + d := r.Data(nil) + d.SetId(aws.StringValue(v.Id)) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping Route 53 Traffic Policy Instance sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Route 53 Traffic Policy Instances (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping Route 53 Traffic Policy Instances (%s): %w", region, err) + } + + return nil +} + func sweepZones(region string) error { client, err := sweep.SharedRegionalSweepClient(region) diff --git a/internal/service/route53/traffic_policy.go b/internal/service/route53/traffic_policy.go new file mode 100644 index 000000000000..c46180382046 --- /dev/null +++ b/internal/service/route53/traffic_policy.go @@ -0,0 +1,192 @@ +package route53 + +import ( + "context" + "fmt" + "log" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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/tfresource" +) + +func ResourceTrafficPolicy() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceTrafficPolicyCreate, + ReadWithoutTimeout: resourceTrafficPolicyRead, + UpdateWithoutTimeout: resourceTrafficPolicyUpdate, + DeleteWithoutTimeout: resourceTrafficPolicyDelete, + + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + const idSeparator = "/" + parts := strings.Split(d.Id(), idSeparator) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, fmt.Errorf("unexpected format for ID (%[1]s), expected TRAFFIC-POLICY-ID%[2]sTRAFFIC-POLICY-VERSION", d.Id(), idSeparator) + } + + version, err := strconv.Atoi(parts[1]) + + if err != nil { + return nil, err + } + + d.SetId(parts[0]) + d.Set("version", version) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "comment": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 1024), + }, + "document": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 102400), + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 512), + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceTrafficPolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + name := d.Get("name").(string) + input := &route53.CreateTrafficPolicyInput{ + Document: aws.String(d.Get("document").(string)), + Name: aws.String(name), + } + + if v, ok := d.GetOk("comment"); ok { + input.Comment = aws.String(v.(string)) + } + + log.Printf("[INFO] Creating Route53 Traffic Policy: %s", input) + outputRaw, err := tfresource.RetryWhenAWSErrCodeEqualsContext(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return conn.CreateTrafficPolicyWithContext(ctx, input) + }, route53.ErrCodeNoSuchTrafficPolicy) + + if err != nil { + return diag.Errorf("error creating Route53 Traffic Policy (%s): %s", name, err) + } + + d.SetId(aws.StringValue(outputRaw.(*route53.CreateTrafficPolicyOutput).TrafficPolicy.Id)) + + return resourceTrafficPolicyRead(ctx, d, meta) +} + +func resourceTrafficPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + trafficPolicy, err := FindTrafficPolicyByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Route53 Traffic Policy %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading Route53 Traffic Policy (%s): %s", d.Id(), err) + } + + d.Set("comment", trafficPolicy.Comment) + d.Set("document", trafficPolicy.Document) + d.Set("name", trafficPolicy.Name) + d.Set("type", trafficPolicy.Type) + d.Set("version", trafficPolicy.Version) + + return nil +} + +func resourceTrafficPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + input := &route53.UpdateTrafficPolicyCommentInput{ + Id: aws.String(d.Id()), + Version: aws.Int64(int64(d.Get("version").(int))), + } + + if d.HasChange("comment") { + input.Comment = aws.String(d.Get("comment").(string)) + } + + log.Printf("[INFO] Updating Route53 Traffic Policy comment: %s", input) + _, err := conn.UpdateTrafficPolicyCommentWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error updating Route53 Traffic Policy (%s) comment: %s", d.Id(), err) + } + + return resourceTrafficPolicyRead(ctx, d, meta) +} + +func resourceTrafficPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + input := &route53.ListTrafficPolicyVersionsInput{ + Id: aws.String(d.Id()), + } + var output []*route53.TrafficPolicy + + err := listTrafficPolicyVersionsPagesWithContext(ctx, conn, input, func(page *route53.ListTrafficPolicyVersionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + output = append(output, page.TrafficPolicies...) + + return !lastPage + }) + + if err != nil { + return diag.Errorf("error listing Route 53 Traffic Policy (%s) versions: %s", d.Id(), err) + } + + for _, v := range output { + version := aws.Int64Value(v.Version) + + log.Printf("[INFO] Delete Route53 Traffic Policy (%s) version: %d", d.Id(), version) + _, err := conn.DeleteTrafficPolicyWithContext(ctx, &route53.DeleteTrafficPolicyInput{ + Id: aws.String(d.Id()), + Version: aws.Int64(version), + }) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchTrafficPolicy) { + continue + } + + if err != nil { + return diag.Errorf("error deleting Route 53 Traffic Policy (%s) version (%d): %s", d.Id(), version, err) + } + } + + return nil +} diff --git a/internal/service/route53/traffic_policy_document_data_source.go b/internal/service/route53/traffic_policy_document_data_source.go new file mode 100644 index 000000000000..ebd02e070fbf --- /dev/null +++ b/internal/service/route53/traffic_policy_document_data_source.go @@ -0,0 +1,621 @@ +package route53 + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/aws/aws-sdk-go/aws" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func DataSourceTrafficPolicyDocument() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceTrafficPolicyDocumentRead, + + Schema: map[string]*schema.Schema{ + "endpoint": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(Route53TrafficPolicyDocEndpointType_Values(), false), + }, + "region": { + Type: schema.TypeString, + Optional: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "json": { + Type: schema.TypeString, + Computed: true, + }, + "record_type": { + Type: schema.TypeString, + Optional: true, + }, + "rule": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + }, + "primary": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_reference": { + Type: schema.TypeString, + Optional: true, + }, + "evaluate_target_health": { + Type: schema.TypeBool, + Optional: true, + }, + "health_check": { + Type: schema.TypeString, + Optional: true, + }, + "rule_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "secondary": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_reference": { + Type: schema.TypeString, + Optional: true, + }, + "evaluate_target_health": { + Type: schema.TypeBool, + Optional: true, + }, + "health_check": { + Type: schema.TypeString, + Optional: true, + }, + "rule_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "location": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "continent": { + Type: schema.TypeString, + Optional: true, + }, + "country": { + Type: schema.TypeString, + Optional: true, + }, + "endpoint_reference": { + Type: schema.TypeString, + Optional: true, + }, + "evaluate_target_health": { + Type: schema.TypeBool, + Optional: true, + }, + "health_check": { + Type: schema.TypeString, + Optional: true, + }, + "is_default": { + Type: schema.TypeBool, + Optional: true, + }, + "rule_reference": { + Type: schema.TypeString, + Optional: true, + }, + "subdivision": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "geo_proximity_location": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bias": { + Type: schema.TypeString, + Optional: true, + }, + "endpoint_reference": { + Type: schema.TypeString, + Optional: true, + }, + "evaluate_target_health": { + Type: schema.TypeBool, + Optional: true, + }, + "health_check": { + Type: schema.TypeString, + Optional: true, + }, + "latitude": { + Type: schema.TypeString, + Optional: true, + }, + "longitude": { + Type: schema.TypeString, + Optional: true, + }, + "region": { + Type: schema.TypeString, + Optional: true, + }, + "rule_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "region": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_reference": { + Type: schema.TypeString, + Optional: true, + }, + "evaluate_target_health": { + Type: schema.TypeBool, + Optional: true, + }, + "health_check": { + Type: schema.TypeString, + Optional: true, + }, + "region": { + Type: schema.TypeString, + Optional: true, + }, + "rule_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "items": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint_reference": { + Type: schema.TypeString, + Optional: true, + }, + "health_check": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "start_endpoint": { + Type: schema.TypeString, + Optional: true, + }, + "start_rule": { + Type: schema.TypeString, + Optional: true, + }, + "version": { + Type: schema.TypeString, + Optional: true, + Default: "2015-10-01", + ValidateFunc: validation.StringInSlice([]string{ + "2015-10-01", + }, false), + }, + }, + } +} + +func dataSourceTrafficPolicyDocumentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + trafficDoc := &Route53TrafficPolicyDoc{} + + if v, ok := d.GetOk("endpoint"); ok { + trafficDoc.Endpoints = expandDataTrafficPolicyEndpointsDoc(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("record_type"); ok { + trafficDoc.RecordType = v.(string) + } + if v, ok := d.GetOk("rule"); ok { + trafficDoc.Rules = expandDataTrafficPolicyRulesDoc(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("start_endpoint"); ok { + trafficDoc.StartEndpoint = v.(string) + } + if v, ok := d.GetOk("start_rule"); ok { + trafficDoc.StartRule = v.(string) + } + if v, ok := d.GetOk("version"); ok { + trafficDoc.AWSPolicyFormatVersion = v.(string) + } + + jsonDoc, err := json.Marshal(trafficDoc) + if err != nil { + return diag.FromErr(err) + } + jsonString := string(jsonDoc) + + d.Set("json", jsonString) + + d.SetId(strconv.Itoa(schema.HashString(jsonString))) + + return nil +} + +func expandDataTrafficPolicyEndpointDoc(tfMap map[string]interface{}) *TrafficPolicyEndpoint { + if tfMap == nil { + return nil + } + + apiObject := &TrafficPolicyEndpoint{} + + if v, ok := tfMap["type"]; ok && v.(string) != "" { + apiObject.Type = v.(string) + } + if v, ok := tfMap["region"]; ok && v.(string) != "" { + apiObject.Region = v.(string) + } + if v, ok := tfMap["value"]; ok && v.(string) != "" { + apiObject.Value = v.(string) + } + + return apiObject +} + +func expandDataTrafficPolicyEndpointsDoc(tfList []interface{}) map[string]*TrafficPolicyEndpoint { + if len(tfList) == 0 { + return nil + } + + apiObjects := make(map[string]*TrafficPolicyEndpoint) + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + id := tfMap["id"].(string) + + apiObject := expandDataTrafficPolicyEndpointDoc(tfMap) + + apiObjects[id] = apiObject + } + + return apiObjects +} + +func expandDataTrafficPolicyRuleDoc(tfMap map[string]interface{}) *TrafficPolicyRule { + if tfMap == nil { + return nil + } + + apiObject := &TrafficPolicyRule{} + + if v, ok := tfMap["type"]; ok && v.(string) != "" { + apiObject.RuleType = v.(string) + } + if v, ok := tfMap["primary"]; ok && len(v.([]interface{})) > 0 { + apiObject.Primary = expandDataTrafficPolicyFailOverDoc(v.([]interface{})) + } + if v, ok := tfMap["secondary"]; ok && len(v.([]interface{})) > 0 { + apiObject.Secondary = expandDataTrafficPolicyFailOverDoc(v.([]interface{})) + } + if v, ok := tfMap["location"]; ok && len(v.(*schema.Set).List()) > 0 { + apiObject.Locations = expandDataTrafficPolicyLocationsDoc(v.(*schema.Set).List()) + } + if v, ok := tfMap["geo_proximity_location"]; ok && len(v.(*schema.Set).List()) > 0 { + apiObject.GeoProximityLocations = expandDataTrafficPolicyProximitiesDoc(v.(*schema.Set).List()) + } + if v, ok := tfMap["region"]; ok && len(v.(*schema.Set).List()) > 0 { + apiObject.Regions = expandDataTrafficPolicyRegionsDoc(v.(*schema.Set).List()) + } + if v, ok := tfMap["items"]; ok && len(v.(*schema.Set).List()) > 0 { + apiObject.Items = expandDataTrafficPolicyItemsDoc(v.(*schema.Set).List()) + } + + return apiObject +} + +func expandDataTrafficPolicyRulesDoc(tfList []interface{}) map[string]*TrafficPolicyRule { + if len(tfList) == 0 { + return nil + } + + apiObjects := make(map[string]*TrafficPolicyRule) + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + id := tfMap["id"].(string) + + apiObject := expandDataTrafficPolicyRuleDoc(tfMap) + + apiObjects[id] = apiObject + } + + return apiObjects +} + +func expandDataTrafficPolicyFailOverDoc(tfList []interface{}) *TrafficPolicyFailoverRule { + if len(tfList) == 0 { + return nil + } + + tfMap, _ := tfList[0].(map[string]interface{}) + + apiObject := &TrafficPolicyFailoverRule{} + + if v, ok := tfMap["endpoint_reference"]; ok && v.(string) != "" { + apiObject.EndpointReference = v.(string) + } + if v, ok := tfMap["rule_reference"]; ok && v.(string) != "" { + apiObject.RuleReference = v.(string) + } + if v, ok := tfMap["evaluate_target_health"]; ok && v.(bool) { + apiObject.EvaluateTargetHealth = aws.Bool(v.(bool)) + } + if v, ok := tfMap["health_check"]; ok && v.(string) != "" { + apiObject.HealthCheck = v.(string) + } + + return apiObject +} + +func expandDataTrafficPolicyLocationDoc(tfMap map[string]interface{}) *TrafficPolicyGeolocationRule { + if tfMap == nil { + return nil + } + + apiObject := &TrafficPolicyGeolocationRule{} + + if v, ok := tfMap["endpoint_reference"]; ok && v.(string) != "" { + apiObject.EndpointReference = v.(string) + } + if v, ok := tfMap["rule_reference"]; ok && v.(string) != "" { + apiObject.RuleReference = v.(string) + } + if v, ok := tfMap["is_default"]; ok && v.(bool) { + apiObject.IsDefault = aws.Bool(v.(bool)) + } + if v, ok := tfMap["continent"]; ok && v.(string) != "" { + apiObject.Continent = v.(string) + } + if v, ok := tfMap["country"]; ok && v.(string) != "" { + apiObject.Country = v.(string) + } + if v, ok := tfMap["subdivision"]; ok && v.(string) != "" { + apiObject.Subdivision = v.(string) + } + if v, ok := tfMap["evaluate_target_health"]; ok && v.(bool) { + apiObject.EvaluateTargetHealth = aws.Bool(v.(bool)) + } + if v, ok := tfMap["health_check"]; ok && v.(string) != "" { + apiObject.HealthCheck = v.(string) + } + + return apiObject +} + +func expandDataTrafficPolicyLocationsDoc(tfList []interface{}) []*TrafficPolicyGeolocationRule { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*TrafficPolicyGeolocationRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandDataTrafficPolicyLocationDoc(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandDataTrafficPolicyProximityDoc(tfMap map[string]interface{}) *TrafficPolicyGeoproximityRule { + if tfMap == nil { + return nil + } + + apiObject := &TrafficPolicyGeoproximityRule{} + + if v, ok := tfMap["endpoint_reference"]; ok && v.(string) != "" { + apiObject.EndpointReference = v.(string) + } + if v, ok := tfMap["rule_reference"]; ok && v.(string) != "" { + apiObject.RuleReference = v.(string) + } + if v, ok := tfMap["region"]; ok && v.(string) != "" { + apiObject.Region = v.(string) + } + if v, ok := tfMap["latitude"]; ok && v.(string) != "" { + apiObject.Latitude = v.(string) + } + if v, ok := tfMap["longitude"]; ok && v.(string) != "" { + apiObject.Longitude = v.(string) + } + if v, ok := tfMap["bias"]; ok && v.(string) != "" { + apiObject.Bias = v.(string) + } + if v, ok := tfMap["evaluate_target_health"]; ok && v.(bool) { + apiObject.EvaluateTargetHealth = aws.Bool(v.(bool)) + } + if v, ok := tfMap["health_check"]; ok && v.(string) != "" { + apiObject.HealthCheck = v.(string) + } + + return apiObject +} + +func expandDataTrafficPolicyProximitiesDoc(tfList []interface{}) []*TrafficPolicyGeoproximityRule { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*TrafficPolicyGeoproximityRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandDataTrafficPolicyProximityDoc(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandDataTrafficPolicyRegionDoc(tfMap map[string]interface{}) *TrafficPolicyLatencyRule { + if tfMap == nil { + return nil + } + + apiObject := &TrafficPolicyLatencyRule{} + + if v, ok := tfMap["endpoint_reference"]; ok && v.(string) != "" { + apiObject.EndpointReference = v.(string) + } + if v, ok := tfMap["rule_reference"]; ok && v.(string) != "" { + apiObject.RuleReference = v.(string) + } + if v, ok := tfMap["region"]; ok && v.(string) != "" { + apiObject.Region = v.(string) + } + if v, ok := tfMap["evaluate_target_health"]; ok && v.(bool) { + apiObject.EvaluateTargetHealth = aws.Bool(v.(bool)) + } + if v, ok := tfMap["health_check"]; ok && v.(string) != "" { + apiObject.HealthCheck = v.(string) + } + + return apiObject +} + +func expandDataTrafficPolicyRegionsDoc(tfList []interface{}) []*TrafficPolicyLatencyRule { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*TrafficPolicyLatencyRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandDataTrafficPolicyRegionDoc(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandDataTrafficPolicyItemDoc(tfMap map[string]interface{}) *TrafficPolicyMultiValueAnswerRule { + if tfMap == nil { + return nil + } + + apiObject := &TrafficPolicyMultiValueAnswerRule{} + + if v, ok := tfMap["endpoint_reference"]; ok && v.(string) != "" { + apiObject.EndpointReference = v.(string) + } + if v, ok := tfMap["health_check"]; ok && v.(string) != "" { + apiObject.HealthCheck = v.(string) + } + + return apiObject +} + +func expandDataTrafficPolicyItemsDoc(tfList []interface{}) []*TrafficPolicyMultiValueAnswerRule { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*TrafficPolicyMultiValueAnswerRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandDataTrafficPolicyItemDoc(tfMap) + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} diff --git a/internal/service/route53/traffic_policy_document_data_source_test.go b/internal/service/route53/traffic_policy_document_data_source_test.go new file mode 100644 index 000000000000..c8b246814624 --- /dev/null +++ b/internal/service/route53/traffic_policy_document_data_source_test.go @@ -0,0 +1,314 @@ +package route53_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/awsutil" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfrouter53 "github.com/hashicorp/terraform-provider-aws/internal/service/route53" +) + +func TestAccRoute53TrafficPolicyDocumentDataSource_basic(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyDocumentDataSourceConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckTrafficPolicySameJSON("data.aws_route53_traffic_policy_document.test", + testAccTrafficPolicyDocumentConfigExpectedJSON()), + ), + }, + }, + }) +} + +func TestAccRoute53TrafficPolicyDocumentDataSource_complete(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyDocumentDataSourceConfigComplete, + Check: resource.ComposeTestCheckFunc( + testAccCheckTrafficPolicySameJSON("data.aws_route53_traffic_policy_document.test", + testAccTrafficPolicyDocumentConfigCompleteExpectedJSON()), + ), + }, + }, + }) +} + +func testAccCheckTrafficPolicySameJSON(resourceName, jsonExpected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + var j, j2 tfrouter53.Route53TrafficPolicyDoc + if err := json.Unmarshal([]byte(rs.Primary.Attributes["json"]), &j); err != nil { + return fmt.Errorf("[ERROR] json.Unmarshal %v", err) + } + if err := json.Unmarshal([]byte(jsonExpected), &j2); err != nil { + return fmt.Errorf("[ERROR] json.Unmarshal %v", err) + } + // Marshall again so it can re order the json data because of arrays + jsonDoc, err := json.Marshal(j) + if err != nil { + return fmt.Errorf("[ERROR] json.marshal %v", err) + } + jsonDoc2, err := json.Marshal(j2) + if err != nil { + return fmt.Errorf("[ERROR] json.marshal %v", err) + } + if err = json.Unmarshal(jsonDoc, &j); err != nil { + return fmt.Errorf("[ERROR] json.Unmarshal %v", err) + } + if err = json.Unmarshal(jsonDoc2, &j); err != nil { + return fmt.Errorf("[ERROR] json.Unmarshal %v", err) + } + + if !awsutil.DeepEqual(&j, &j2) { + return fmt.Errorf("expected out to be %v, got %v", j, j2) + } + + return nil + } +} + +func testAccTrafficPolicyDocumentConfigCompleteExpectedJSON() string { + return fmt.Sprintf(`{ + "AWSPolicyFormatVersion":"2015-10-01", + "RecordType":"A", + "StartRule":"geo_restriction", + "Endpoints":{ + "east_coast_lb1":{ + "Type":"elastic-load-balancer", + "Value":"elb-111111.%[1]s.elb.amazonaws.com" + }, + "east_coast_lb2":{ + "Type":"elastic-load-balancer", + "Value":"elb-222222.%[1]s.elb.amazonaws.com" + }, + "west_coast_lb1":{ + "Type":"elastic-load-balancer", + "Value":"elb-111111.%[2]s.elb.amazonaws.com" + }, + "west_coast_lb2":{ + "Type":"elastic-load-balancer", + "Value":"elb-222222.%[2]s.elb.amazonaws.com" + }, + "denied_message":{ + "Type":"s3-website", + "Region":"%[1]s", + "Value":"video.example.com" + } + }, + "Rules":{ + "geo_restriction":{ + "RuleType":"geo", + "Locations":[ + { + "EndpointReference":"denied_message", + "IsDefault":true + }, + { + "RuleReference":"region_selector", + "Country":"US" + } + ] + }, + "region_selector":{ + "RuleType":"latency", + "Regions":[ + { + "Region":"%[1]s", + "RuleReference":"east_coast_region" + }, + { + "Region":"%[2]s", + "RuleReference":"west_coast_region" + } + ] + }, + "east_coast_region":{ + "RuleType":"failover", + "Primary":{ + "EndpointReference":"east_coast_lb1" + }, + "Secondary":{ + "EndpointReference":"east_coast_lb2" + } + }, + "west_coast_region":{ + "RuleType":"failover", + "Primary":{ + "EndpointReference":"west_coast_lb1" + }, + "Secondary":{ + "EndpointReference":"west_coast_lb2" + } + } + } +}`, acctest.Region(), acctest.AlternateRegion()) +} + +const testAccTrafficPolicyDocumentDataSourceConfig = ` +data "aws_region" "current" {} + +data "aws_route53_traffic_policy_document" "test" { + record_type = "A" + start_rule = "site_switch" + + endpoint { + id = "my_elb" + type = "elastic-load-balancer" + value = "elb-111111.${data.aws_region.current.name}.elb.amazonaws.com" + } + endpoint { + id = "site_down_banner" + type = "s3-website" + region = data.aws_region.current.name + value = "www.example.com" + } + + rule { + id = "site_switch" + type = "failover" + + primary { + endpoint_reference = "my_elb" + } + secondary { + endpoint_reference = "site_down_banner" + } + } +} +` + +const testAccTrafficPolicyDocumentDataSourceConfigComplete = ` +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_route53_traffic_policy_document" "test" { + version = "2015-10-01" + record_type = "A" + start_rule = "geo_restriction" + + endpoint { + id = "east_coast_lb1" + type = "elastic-load-balancer" + value = "elb-111111.${data.aws_availability_zones.available.names[0]}.elb.amazonaws.com" + } + endpoint { + id = "east_coast_lb2" + type = "elastic-load-balancer" + value = "elb-222222.${data.aws_availability_zones.available.names[0]}.elb.amazonaws.com" + } + endpoint { + id = "west_coast_lb1" + type = "elastic-load-balancer" + value = "elb-111111.${data.aws_availability_zones.available.names[1]}.elb.amazonaws.com" + } + endpoint { + id = "west_coast_lb2" + type = "elastic-load-balancer" + value = "elb-222222.${data.aws_availability_zones.available.names[1]}.elb.amazonaws.com" + } + endpoint { + id = "denied_message" + type = "s3-website" + region = data.aws_availability_zones.available.names[0] + value = "video.example.com" + } + + rule { + id = "geo_restriction" + type = "geo" + + location { + endpoint_reference = "denied_message" + is_default = true + } + location { + rule_reference = "region_selector" + country = "US" + } + } + rule { + id = "region_selector" + type = "latency" + + region { + region = data.aws_availability_zones.available.names[0] + rule_reference = "east_coast_region" + } + region { + region = data.aws_availability_zones.available.names[1] + rule_reference = "west_coast_region" + } + } + rule { + id = "east_coast_region" + type = "failover" + + primary { + endpoint_reference = "east_coast_lb1" + } + secondary { + endpoint_reference = "east_coast_lb2" + } + } + rule { + id = "west_coast_region" + type = "failover" + + primary { + endpoint_reference = "west_coast_lb1" + } + secondary { + endpoint_reference = "west_coast_lb2" + } + } +} +` + +func testAccTrafficPolicyDocumentConfigExpectedJSON() string { + return fmt.Sprintf(`{ + "AWSPolicyFormatVersion":"2015-10-01", + "RecordType":"A", + "StartRule":"site_switch", + "Endpoints":{ + "my_elb":{ + "Type":"elastic-load-balancer", + "Value":"elb-111111.%[1]s.elb.amazonaws.com" + }, + "site_down_banner":{ + "Type":"s3-website", + "Region":"%[1]s", + "Value":"www.example.com" + } + }, + "Rules":{ + "site_switch":{ + "RuleType":"failover", + "Primary":{ + "EndpointReference":"my_elb" + }, + "Secondary":{ + "EndpointReference":"site_down_banner" + } + } + } +}`, acctest.Region()) +} diff --git a/internal/service/route53/traffic_policy_document_model.go b/internal/service/route53/traffic_policy_document_model.go new file mode 100644 index 000000000000..386867f2033d --- /dev/null +++ b/internal/service/route53/traffic_policy_document_model.go @@ -0,0 +1,85 @@ +package route53 + +const ( + Route53TrafficPolicyDocEndpointValue = "value" + Route53TrafficPolicyDocEndpointCloudfront = "cloudfront" + Route53TrafficPolicyDocEndpointElastic = "elastic-load-balancer" + Route53TrafficPolicyDocEndpointS3 = "s3-website" +) + +// Route53TrafficPolicyDocEndpointType_Values returns all elements of the endpoints types +func Route53TrafficPolicyDocEndpointType_Values() []string { + return []string{ + Route53TrafficPolicyDocEndpointValue, + Route53TrafficPolicyDocEndpointCloudfront, + Route53TrafficPolicyDocEndpointElastic, + Route53TrafficPolicyDocEndpointS3, + } +} + +type Route53TrafficPolicyDoc struct { + AWSPolicyFormatVersion string `json:",omitempty"` + RecordType string `json:",omitempty"` + StartEndpoint string `json:",omitempty"` + StartRule string `json:",omitempty"` + Endpoints map[string]*TrafficPolicyEndpoint `json:",omitempty"` + Rules map[string]*TrafficPolicyRule `json:",omitempty"` +} + +type TrafficPolicyEndpoint struct { + Type string `json:",omitempty"` + Region string `json:",omitempty"` + Value string `json:",omitempty"` +} + +type TrafficPolicyRule struct { + RuleType string `json:",omitempty"` + Primary *TrafficPolicyFailoverRule `json:",omitempty"` + Secondary *TrafficPolicyFailoverRule `json:",omitempty"` + Locations []*TrafficPolicyGeolocationRule `json:",omitempty"` + GeoProximityLocations []*TrafficPolicyGeoproximityRule `json:",omitempty"` + Regions []*TrafficPolicyLatencyRule `json:",omitempty"` + Items []*TrafficPolicyMultiValueAnswerRule `json:",omitempty"` +} + +type TrafficPolicyFailoverRule struct { + EndpointReference string `json:",omitempty"` + RuleReference string `json:",omitempty"` + EvaluateTargetHealth *bool `json:",omitempty"` + HealthCheck string `json:",omitempty"` +} + +type TrafficPolicyGeolocationRule struct { + EndpointReference string `json:",omitempty"` + RuleReference string `json:",omitempty"` + IsDefault *bool `json:",omitempty"` + Continent string `json:",omitempty"` + Country string `json:",omitempty"` + Subdivision string `json:",omitempty"` + EvaluateTargetHealth *bool `json:",omitempty"` + HealthCheck string `json:",omitempty"` +} + +type TrafficPolicyGeoproximityRule struct { + EndpointReference string `json:",omitempty"` + RuleReference string `json:",omitempty"` + Region string `json:",omitempty"` + Latitude string `json:",omitempty"` + Longitude string `json:",omitempty"` + Bias string `json:",omitempty"` + EvaluateTargetHealth *bool `json:",omitempty"` + HealthCheck string `json:",omitempty"` +} + +type TrafficPolicyLatencyRule struct { + EndpointReference string `json:",omitempty"` + RuleReference string `json:",omitempty"` + Region string `json:",omitempty"` + EvaluateTargetHealth *bool `json:",omitempty"` + HealthCheck string `json:",omitempty"` +} + +type TrafficPolicyMultiValueAnswerRule struct { + EndpointReference string `json:",omitempty"` + HealthCheck string `json:",omitempty"` +} diff --git a/internal/service/route53/traffic_policy_instance.go b/internal/service/route53/traffic_policy_instance.go new file mode 100644 index 000000000000..33e8e43d2ca8 --- /dev/null +++ b/internal/service/route53/traffic_policy_instance.go @@ -0,0 +1,164 @@ +package route53 + +import ( + "context" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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/tfresource" +) + +func ResourceTrafficPolicyInstance() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceTrafficPolicyInstanceCreate, + ReadWithoutTimeout: resourceTrafficPolicyInstanceRead, + UpdateWithoutTimeout: resourceTrafficPolicyInstanceUpdate, + DeleteWithoutTimeout: resourceTrafficPolicyInstanceDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "hosted_zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 32), + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 1024), + StateFunc: func(v interface{}) string { + value := strings.TrimSuffix(v.(string), ".") + return strings.ToLower(value) + }, + }, + "traffic_policy_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 36), + }, + "traffic_policy_version": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 1000), + }, + "ttl": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntAtMost(2147483647), + }, + }, + } +} + +func resourceTrafficPolicyInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + name := d.Get("name").(string) + input := &route53.CreateTrafficPolicyInstanceInput{ + HostedZoneId: aws.String(d.Get("hosted_zone_id").(string)), + Name: aws.String(name), + TrafficPolicyId: aws.String(d.Get("traffic_policy_id").(string)), + TrafficPolicyVersion: aws.Int64(int64(d.Get("traffic_policy_version").(int))), + TTL: aws.Int64(int64(d.Get("ttl").(int))), + } + + log.Printf("[INFO] Creating Route53 Traffic Policy Instance: %s", input) + outputRaw, err := tfresource.RetryWhenAWSErrCodeEqualsContext(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return conn.CreateTrafficPolicyInstanceWithContext(ctx, input) + }, route53.ErrCodeNoSuchTrafficPolicy) + + if err != nil { + return diag.Errorf("error creating Route53 Traffic Policy Instance (%s): %s", name, err) + } + + d.SetId(aws.StringValue(outputRaw.(*route53.CreateTrafficPolicyInstanceOutput).TrafficPolicyInstance.Id)) + + if _, err = waitTrafficPolicyInstanceStateCreated(ctx, conn, d.Id()); err != nil { + return diag.Errorf("error waiting for Route53 Traffic Policy Instance (%s) create: %s", d.Id(), err) + } + + return resourceTrafficPolicyInstanceRead(ctx, d, meta) +} + +func resourceTrafficPolicyInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + trafficPolicyInstance, err := FindTrafficPolicyInstanceByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Route53 Traffic Policy Instance %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("error reading Route53 Traffic Policy Instance (%s): %s", d.Id(), err) + } + + d.Set("hosted_zone_id", trafficPolicyInstance.HostedZoneId) + d.Set("name", strings.TrimSuffix(aws.StringValue(trafficPolicyInstance.Name), ".")) + d.Set("traffic_policy_id", trafficPolicyInstance.TrafficPolicyId) + d.Set("traffic_policy_version", trafficPolicyInstance.TrafficPolicyVersion) + d.Set("ttl", trafficPolicyInstance.TTL) + + return nil +} + +func resourceTrafficPolicyInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + input := &route53.UpdateTrafficPolicyInstanceInput{ + Id: aws.String(d.Id()), + TrafficPolicyId: aws.String(d.Get("traffic_policy_id").(string)), + TrafficPolicyVersion: aws.Int64(int64(d.Get("traffic_policy_version").(int))), + TTL: aws.Int64(int64(d.Get("ttl").(int))), + } + + log.Printf("[INFO] Updating Route53 Traffic Policy Instance: %s", input) + _, err := conn.UpdateTrafficPolicyInstanceWithContext(ctx, input) + + if err != nil { + return diag.Errorf("error updating Route53 Traffic Policy Instance (%s): %s", d.Id(), err) + } + + if _, err = waitTrafficPolicyInstanceStateUpdated(ctx, conn, d.Id()); err != nil { + return diag.Errorf("error waiting for Route53 Traffic Policy Instance (%s) update: %s", d.Id(), err) + } + + return resourceTrafficPolicyInstanceRead(ctx, d, meta) +} + +func resourceTrafficPolicyInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).Route53Conn + + log.Printf("[INFO] Delete Route53 Traffic Policy Instance: %s", d.Id()) + _, err := conn.DeleteTrafficPolicyInstanceWithContext(ctx, &route53.DeleteTrafficPolicyInstanceInput{ + Id: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, route53.ErrCodeNoSuchTrafficPolicyInstance) { + return nil + } + + if err != nil { + return diag.Errorf("error deleting Route53 Traffic Policy Instance (%s): %s", d.Id(), err) + } + + if _, err = waitTrafficPolicyInstanceStateDeleted(ctx, conn, d.Id()); err != nil { + return diag.Errorf("error waiting for Route53 Traffic Policy Instance (%s) delete: %s", d.Id(), err) + } + + return nil +} diff --git a/internal/service/route53/traffic_policy_instance_test.go b/internal/service/route53/traffic_policy_instance_test.go new file mode 100644 index 000000000000..4f1844a677b2 --- /dev/null +++ b/internal/service/route53/traffic_policy_instance_test.go @@ -0,0 +1,194 @@ +package route53_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/aws/aws-sdk-go/service/route53" + 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/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfroute53 "github.com/hashicorp/terraform-provider-aws/internal/service/route53" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func testAccPreCheckRoute53TrafficPolicy(t *testing.T) { + acctest.PreCheckPartitionHasService(route53.EndpointsID, t) + + if got, want := acctest.Partition(), endpoints.AwsUsGovPartitionID; got == want { + t.Skipf("Route 53 Traffic Policies are not supported in %s partition", got) + } +} + +func TestAccRoute53TrafficPolicyInstance_basic(t *testing.T) { + var v route53.TrafficPolicyInstance + resourceName := "aws_route53_traffic_policy_instance.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + zoneName := acctest.RandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckRoute53TrafficPolicy(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckRoute53TrafficPolicyInstanceDestroy, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyInstanceConfig(rName, zoneName, 3600), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyInstanceExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("%s.%s", rName, zoneName)), + resource.TestCheckResourceAttr(resourceName, "ttl", "3600"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRoute53TrafficPolicyInstance_disappears(t *testing.T) { + var v route53.TrafficPolicyInstance + resourceName := "aws_route53_traffic_policy_instance.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + zoneName := acctest.RandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckRoute53TrafficPolicy(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckRoute53TrafficPolicyInstanceDestroy, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyInstanceConfig(rName, zoneName, 360), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyInstanceExists(resourceName, &v), + acctest.CheckResourceDisappears(acctest.Provider, tfroute53.ResourceTrafficPolicyInstance(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccRoute53TrafficPolicyInstance_update(t *testing.T) { + var v route53.TrafficPolicyInstance + resourceName := "aws_route53_traffic_policy_instance.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + zoneName := acctest.RandomDomainName() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckRoute53TrafficPolicy(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckRoute53TrafficPolicyInstanceDestroy, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyInstanceConfig(rName, zoneName, 3600), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyInstanceExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "ttl", "3600"), + ), + }, + { + Config: testAccTrafficPolicyInstanceConfig(rName, zoneName, 7200), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyInstanceExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "ttl", "7200"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckRoute53TrafficPolicyInstanceExists(n string, v *route53.TrafficPolicyInstance) 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 Route53 Traffic Policy Instance ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).Route53Conn + + output, err := tfroute53.FindTrafficPolicyInstanceByID(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckRoute53TrafficPolicyInstanceDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).Route53Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_traffic_policy_instance" { + continue + } + + _, err := tfroute53.FindTrafficPolicyInstanceByID(context.Background(), conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Route53 Traffic Policy Instance %s still exists", rs.Primary.ID) + } + return nil +} + +func testAccTrafficPolicyInstanceConfig(rName, zoneName string, ttl int) string { + return fmt.Sprintf(` +resource "aws_route53_zone" "test" { + name = %[2]q +} + +resource "aws_route53_traffic_policy" "test" { + name = %[1]q + document = <<-EOT +{ + "AWSPolicyFormatVersion":"2015-10-01", + "RecordType":"A", + "Endpoints":{ + "endpoint-start-NkPh":{ + "Type":"value", + "Value":"10.0.0.1" + } + }, + "StartEndpoint":"endpoint-start-NkPh" +} +EOT +} + +resource "aws_route53_traffic_policy_instance" "test" { + hosted_zone_id = aws_route53_zone.test.zone_id + name = "%[1]s.%[2]s" + traffic_policy_id = aws_route53_traffic_policy.test.id + traffic_policy_version = aws_route53_traffic_policy.test.version + ttl = %[3]d +} +`, rName, zoneName, ttl) +} diff --git a/internal/service/route53/traffic_policy_test.go b/internal/service/route53/traffic_policy_test.go new file mode 100644 index 000000000000..458a934db00c --- /dev/null +++ b/internal/service/route53/traffic_policy_test.go @@ -0,0 +1,209 @@ +package route53_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/route53" + 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/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfroute53 "github.com/hashicorp/terraform-provider-aws/internal/service/route53" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccRoute53TrafficPolicy_basic(t *testing.T) { + var v route53.TrafficPolicy + resourceName := "aws_route53_traffic_policy.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckRoute53TrafficPolicy(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckRoute53TrafficPolicyDestroy, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckRoute53TrafficPolicyExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "comment", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "type", "A"), + resource.TestCheckResourceAttr(resourceName, "version", "1"), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: testAccTrafficPolicyImportStateIdFunc(resourceName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccRoute53TrafficPolicy_disappears(t *testing.T) { + var v route53.TrafficPolicy + resourceName := "aws_route53_traffic_policy.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckRoute53TrafficPolicy(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckRoute53TrafficPolicyDestroy, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyExists(resourceName, &v), + acctest.CheckResourceDisappears(acctest.Provider, tfroute53.ResourceTrafficPolicy(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccRoute53TrafficPolicy_update(t *testing.T) { + var v route53.TrafficPolicy + resourceName := "aws_route53_traffic_policy.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + comment := `comment` + commentUpdated := `comment updated` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckRoute53TrafficPolicy(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckRoute53TrafficPolicyDestroy, + ErrorCheck: acctest.ErrorCheck(t, route53.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccTrafficPolicyConfigComplete(rName, comment), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "comment", comment), + ), + }, + { + Config: testAccTrafficPolicyConfigComplete(rName, commentUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53TrafficPolicyExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "comment", commentUpdated), + ), + }, + { + ResourceName: resourceName, + ImportStateIdFunc: testAccTrafficPolicyImportStateIdFunc(resourceName), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckRoute53TrafficPolicyExists(n string, v *route53.TrafficPolicy) 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 Route53 Traffic Policy ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).Route53Conn + + output, err := tfroute53.FindTrafficPolicyByID(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckRoute53TrafficPolicyDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).Route53Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_traffic_policy" { + continue + } + + _, err := tfroute53.FindTrafficPolicyByID(context.Background(), conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Route53 Traffic Policy %s still exists", rs.Primary.ID) + } + return nil +} + +func testAccTrafficPolicyImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("not found: %s", resourceName) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["id"], rs.Primary.Attributes["version"]), nil + } +} + +func testAccTrafficPolicyConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_route53_traffic_policy" "test" { + name = %[1]q + document = <<-EOT +{ + "AWSPolicyFormatVersion":"2015-10-01", + "RecordType":"A", + "Endpoints":{ + "endpoint-start-NkPh":{ + "Type":"value", + "Value":"10.0.0.1" + } + }, + "StartEndpoint":"endpoint-start-NkPh" +} +EOT +} +`, rName) +} + +func testAccTrafficPolicyConfigComplete(rName, comment string) string { + return fmt.Sprintf(` +resource "aws_route53_traffic_policy" "test" { + name = %[1]q + comment = %[2]q + document = <<-EOT +{ + "AWSPolicyFormatVersion":"2015-10-01", + "RecordType":"A", + "Endpoints":{ + "endpoint-start-NkPh":{ + "Type":"value", + "Value":"10.0.0.1" + } + }, + "StartEndpoint":"endpoint-start-NkPh" +} +EOT +} +`, rName, comment) +} diff --git a/internal/service/route53/wait.go b/internal/service/route53/wait.go index c7265a4bcf23..c47a81ef03d0 100644 --- a/internal/service/route53/wait.go +++ b/internal/service/route53/wait.go @@ -1,14 +1,15 @@ package route53 import ( + "context" "errors" - "fmt" "math/rand" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) const ( @@ -21,6 +22,8 @@ const ( hostedZoneDNSSECStatusTimeout = 5 * time.Minute keySigningKeyStatusTimeout = 5 * time.Minute + + trafficPolicyInstanceOperationTimeout = 4 * time.Minute ) func waitChangeInfoStatusInsync(conn *route53.Route53, changeID string) (*route53.ChangeInfo, error) { //nolint:unparam @@ -58,19 +61,8 @@ func waitHostedZoneDNSSECStatusUpdated(conn *route53.Route53, hostedZoneID strin outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*route53.DNSSECStatus); ok { - if err != nil && output != nil && output.ServeSignature != nil && output.StatusMessage != nil { - newErr := fmt.Errorf("%s: %s", aws.StringValue(output.ServeSignature), aws.StringValue(output.StatusMessage)) - - switch e := err.(type) { - case *resource.TimeoutError: - if e.LastError == nil { - e.LastError = newErr - } - case *resource.UnexpectedStateError: - if e.LastError == nil { - e.LastError = newErr - } - } + if serveSignature := aws.StringValue(output.ServeSignature); serveSignature == ServeSignatureInternalFailure { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) } return output, err @@ -90,16 +82,71 @@ func waitKeySigningKeyStatusUpdated(conn *route53.Route53, hostedZoneID string, outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*route53.KeySigningKey); ok { - if err != nil && output != nil && output.Status != nil && output.StatusMessage != nil { - newErr := fmt.Errorf("%s: %s", aws.StringValue(output.Status), aws.StringValue(output.StatusMessage)) - - var te *resource.TimeoutError - var use *resource.UnexpectedStateError - if ok := errors.As(err, &te); ok && te.LastError == nil { - te.LastError = newErr - } else if ok := errors.As(err, &use); ok && use.LastError == nil { - use.LastError = newErr - } + if status := aws.StringValue(output.Status); status == KeySigningKeyStatusInternalFailure { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusMessage))) + } + + return output, err + } + + return nil, err +} + +func waitTrafficPolicyInstanceStateCreated(ctx context.Context, conn *route53.Route53, id string) (*route53.TrafficPolicyInstance, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{TrafficPolicyInstanceStateCreating}, + Target: []string{TrafficPolicyInstanceStateApplied}, + Refresh: statusTrafficPolicyInstanceState(ctx, conn, id), + Timeout: trafficPolicyInstanceOperationTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*route53.TrafficPolicyInstance); ok { + if state := aws.StringValue(output.State); state == TrafficPolicyInstanceStateFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.Message))) + } + + return output, err + } + + return nil, err +} + +func waitTrafficPolicyInstanceStateDeleted(ctx context.Context, conn *route53.Route53, id string) (*route53.TrafficPolicyInstance, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{TrafficPolicyInstanceStateDeleting}, + Target: []string{}, + Refresh: statusTrafficPolicyInstanceState(ctx, conn, id), + Timeout: trafficPolicyInstanceOperationTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*route53.TrafficPolicyInstance); ok { + if state := aws.StringValue(output.State); state == TrafficPolicyInstanceStateFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.Message))) + } + + return output, err + } + + return nil, err +} + +func waitTrafficPolicyInstanceStateUpdated(ctx context.Context, conn *route53.Route53, id string) (*route53.TrafficPolicyInstance, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{TrafficPolicyInstanceStateUpdating}, + Target: []string{TrafficPolicyInstanceStateApplied}, + Refresh: statusTrafficPolicyInstanceState(ctx, conn, id), + Timeout: trafficPolicyInstanceOperationTimeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*route53.TrafficPolicyInstance); ok { + if state := aws.StringValue(output.State); state == TrafficPolicyInstanceStateFailed { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.Message))) } return output, err diff --git a/website/docs/d/route53_traffic_policy_document.html.markdown b/website/docs/d/route53_traffic_policy_document.html.markdown new file mode 100644 index 000000000000..1ea54de1896f --- /dev/null +++ b/website/docs/d/route53_traffic_policy_document.html.markdown @@ -0,0 +1,135 @@ +--- +subcategory: "Route53" +layout: "aws" +page_title: "AWS: aws_route53_traffic_policy_document" +description: |- + Generates an Route53 traffic policy document in JSON format +--- + +# Data Source: aws_route53_traffic_policy_document + +Generates an Route53 traffic policy document in JSON format for use with resources that expect policy documents such as [`aws_route53_traffic_policy`](/docs/providers/aws/r/route53_traffic_policy.html). + + +## Example Usage + +### Basic Example + +```terraform +data "aws_region" "current" {} + +data "aws_route53_traffic_policy_document" "example" { + record_type = "A" + start_rule = "site_switch" + + endpoint { + id = "my_elb" + type = "elastic-load-balancer" + value = "elb-111111.${data.aws_region.current.name}.elb.amazonaws.com" + } + endpoint { + id = "site_down_banner" + type = "s3-website" + region = data.aws_region.current.name + value = "www.example.com" + } + + rule { + id = "site_switch" + type = "failover" + + primary { + endpoint_reference = "my_elb" + } + secondary { + endpoint_reference = "site_down_banner" + } + } +} + +resource "aws_route53_traffic_policy" "example" { + name = "example" + comment = "example comment" + document = data.aws_route53_traffic_policy_document.example.json +} +``` + +## Argument Reference + +The following arguments are optional: + +* `endpoint` (Optional) - Configuration block for the definitions of the endpoints that you want to use in this traffic policy. See below +* `record_type` (Optional) - DNS type of all of the resource record sets that Amazon Route 53 will create based on this traffic policy. +* `rule` (Optional) - Configuration block for definitions of the rules that you want to use in this traffic policy. See below +* `start_endpoint` (Optional) - An endpoint to be as the starting point for the traffic policy. +* `start_rule` (Optional) - A rule to be as the starting point for the traffic policy. +* `version` (Optional) - Version of the traffic policy format. + + +### `endpoint` + +* `id` - (Required) ID of an endpoint you want to assign. +* `type` - (Optional) Type of the endpoint. Valid values are `value` , `cloudfront` , `elastic-load-balancer`, `s3-website` +* `region` - (Optional) To route traffic to an Amazon S3 bucket that is configured as a website endpoint, specify the region in which you created the bucket for `region`. +* `value` - (Optional) Value of the `type`. + + +### `rule` + +* `id` - (Required) ID of a rule you want to assign. +* `type` - (Optional) Type of the rule. +* `primary` - (Optional) Configuration block for the settings for the rule or endpoint that you want to route traffic to whenever the corresponding resources are available. Only valid for `failover` type. See below +* `secondary` - (Optional) Configuration block for the rule or endpoint that you want to route traffic to whenever the primary resources are not available. Only valid for `failover` type. See below +* `location` - (Optional) Configuration block for when you add a geolocation rule, you configure your traffic policy to route your traffic based on the geographic location of your users. Only valid for `geo` type. See below +* `geo_proximity_location` - (Optional) Configuration block for when you add a geoproximity rule, you configure Amazon Route 53 to route traffic to your resources based on the geographic location of your resources. Only valid for `geoproximity` type. See below +* `regions` - (Optional) Configuration block for when you add a latency rule, you configure your traffic policy to route your traffic based on the latency (the time delay) between your users and the AWS regions where you've created AWS resources such as ELB load balancers and Amazon S3 buckets. Only valid for `latency` type. See below +* `items` - (Optional) Configuration block for when you add a multivalue answer rule, you configure your traffic policy to route traffic approximately randomly to your healthy resources. Only valid for `multivalue` type. See below + +### `primary` and `secondary` + +* `endpoint_reference` - (Optional) References to an endpoint. +* `evaluate_target_health` - (Optional) Indicates whether you want Amazon Route 53 to evaluate the health of the endpoint and route traffic only to healthy endpoints. +* `health_check` - (Optional) If you want to associate a health check with the endpoint or rule. +* `rule_reference` - (Optional) References to a rule. + +### `location` + +* `continent` - (Optional) Value of a continent. +* `country` - (Optional) Value of a country. +* `endpoint_reference` - (Optional) References to an endpoint. +* `evaluate_target_health` - (Optional) Indicates whether you want Amazon Route 53 to evaluate the health of the endpoint and route traffic only to healthy endpoints. +* `health_check` - (Optional) If you want to associate a health check with the endpoint or rule. +* `is_default` - (Optional) Indicates whether this set of values represents the default location. +* `rule_reference` - (Optional) References to a rule. +* `subdivision` - (Optional) Value of a subdivision. + +### `geo_proximity_location` + +* `bias` - (Optional) Specify a value for `bias` if you want to route more traffic to an endpoint from nearby endpoints (positive values) or route less traffic to an endpoint (negative values). +* `endpoint_reference` - (Optional) References to an endpoint. +* `evaluate_target_health` - (Optional) Indicates whether you want Amazon Route 53 to evaluate the health of the endpoint and route traffic only to healthy endpoints. +* `health_check` - (Optional) If you want to associate a health check with the endpoint or rule. +* `latitude` - (Optional) Represents the location south (negative) or north (positive) of the equator. Valid values are -90 degrees to 90 degrees. +* `longitude` - (Optional) Represents the location west (negative) or east (positive) of the prime meridian. Valid values are -180 degrees to 180 degrees. +* `region` - (Optional) If your endpoint is an AWS resource, specify the AWS Region that you created the resource in. +* `rule_reference` - (Optional) References to a rule. + +### `region` + +* `endpoint_reference` - (Optional) References to an endpoint. +* `evaluate_target_health` - (Optional) Indicates whether you want Amazon Route 53 to evaluate the health of the endpoint and route traffic only to healthy endpoints. +* `health_check` - (Optional) If you want to associate a health check with the endpoint or rule. +* `region` - (Optional) Region code for the AWS Region that you created the resource in. +* `rule_reference` - (Optional) References to a rule. + +### `item` + +* `endpoint_reference` - (Optional) References to an endpoint. +* `health_check` - (Optional) If you want to associate a health check with the endpoint or rule. + + +## Attributes Reference + +The following attribute is exported: + +* `json` - Standard JSON policy document rendered based on the arguments above. diff --git a/website/docs/r/route53_traffic_policy.html.markdown b/website/docs/r/route53_traffic_policy.html.markdown new file mode 100644 index 000000000000..74783f2689ee --- /dev/null +++ b/website/docs/r/route53_traffic_policy.html.markdown @@ -0,0 +1,61 @@ +--- +subcategory: "Route53" +layout: "aws" +page_title: "AWS: aws_route53_traffic_policy" +description: |- + Manages a Route53 Traffic Policy +--- + +# Resource: aws_route53_traffic_policy + +Manages a Route53 Traffic Policy. + +## Example Usage + +```terraform +resource "aws_route53_traffic_policy" "example" { + name = "example" + comment = "example comment" + document = <