diff --git a/.changelog/43955.txt b/.changelog/43955.txt new file mode 100644 index 000000000000..8e94f5403e01 --- /dev/null +++ b/.changelog/43955.txt @@ -0,0 +1,3 @@ +```release-note:new-action +aws_cloudfront_create_invalidation +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 026dbb81080e..ae13c0637878 100644 --- a/go.mod +++ b/go.mod @@ -289,7 +289,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hcl/v2 v2.23.0 + github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-framework v1.16.0 github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 @@ -300,7 +300,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.21.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 - github.com/hashicorp/terraform-plugin-testing v1.13.3 + github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247 github.com/jaswdr/faker/v2 v2.8.0 github.com/jmespath/go-jmespath v0.4.0 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -349,7 +349,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.23.0 // indirect + github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 // indirect github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect @@ -368,7 +368,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/zclconf/go-cty v1.16.4 // indirect + github.com/zclconf/go-cty v1.17.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/go.sum b/go.sum index 45428faddc78..fa03a69bd1c7 100644 --- a/go.sum +++ b/go.sum @@ -661,12 +661,12 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= -github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= -github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= -github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-framework v1.16.0 h1:tP0f+yJg0Z672e7levixDe5EpWwrTrNryPM9kDMYIpE= @@ -685,8 +685,8 @@ github.com/hashicorp/terraform-plugin-mux v0.21.0 h1:QsEYnzSD2c3zT8zUrUGqaFGhV/Z github.com/hashicorp/terraform-plugin-mux v0.21.0/go.mod h1:Qpt8+6AD7NmL0DS7ASkN0EXpDQ2J/FnnIgeUr1tzr5A= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= -github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo= -github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= +github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247 h1:lA6ofPwmCXAX7J7kVP9t/WMU5+eA4e9YvJUiRLPdENw= +github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247/go.mod h1:4r/7cxl1mpskfALcq58Iyu5aPiTSco8SVrKkcLyP5g4= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -790,8 +790,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= -github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/internal/service/cloudfront/create_invalidation_action.go b/internal/service/cloudfront/create_invalidation_action.go new file mode 100644 index 000000000000..0cd271486ef5 --- /dev/null +++ b/internal/service/cloudfront/create_invalidation_action.go @@ -0,0 +1,300 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" + "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @Action(aws_cloudfront_create_invalidation, name="Create Invalidation") +func newCreateInvalidationAction(_ context.Context) (action.ActionWithConfigure, error) { + return &createInvalidationAction{}, nil +} + +var ( + _ action.Action = (*createInvalidationAction)(nil) +) + +type createInvalidationAction struct { + framework.ActionWithModel[createInvalidationModel] +} + +type createInvalidationModel struct { + DistributionID types.String `tfsdk:"distribution_id"` + Paths fwtypes.ListOfString `tfsdk:"paths"` + CallerReference types.String `tfsdk:"caller_reference"` + Timeout types.Int64 `tfsdk:"timeout"` +} + +func (a *createInvalidationAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Invalidates CloudFront distribution cache for specified paths. This action creates an invalidation request and waits for it to complete.", + Attributes: map[string]schema.Attribute{ + "distribution_id": schema.StringAttribute{ + Description: "The ID of the CloudFront distribution to invalidate cache for", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexache.MustCompile(`^[A-Z0-9]+$`), + "must be a valid CloudFront distribution ID (e.g., E1GHKQ2EXAMPLE)", + ), + }, + }, + "paths": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + Description: "List of file paths or patterns to invalidate. Use /* to invalidate all files", + Required: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + listvalidator.SizeAtMost(3000), // CloudFront limit + }, + }, + "caller_reference": schema.StringAttribute{ + Description: "Unique identifier for the invalidation request. If not provided, one will be generated automatically", + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(128), + }, + }, + names.AttrTimeout: schema.Int64Attribute{ + Description: "Timeout in seconds to wait for the invalidation to complete (default: 900)", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(60), + int64validator.AtMost(3600), + }, + }, + }, + } +} + +func (a *createInvalidationAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config createInvalidationModel + + // Parse configuration + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // Get AWS client + conn := a.Meta().CloudFrontClient(ctx) + + distributionID := config.DistributionID.ValueString() + + // Convert paths list to string slice + var paths []string + resp.Diagnostics.Append(config.Paths.ElementsAs(ctx, &paths, false)...) + if resp.Diagnostics.HasError() { + return + } + + // Validate paths + for _, path := range paths { + if path == "" { + resp.Diagnostics.AddError( + "Invalid Path", + "Path cannot be empty", + ) + return + } + if !regexache.MustCompile(`^(/.*|\*)$`).MatchString(path) { + resp.Diagnostics.AddError( + "Invalid Path Format", + fmt.Sprintf("Path '%s' must start with '/' or be '*' for all files", path), + ) + return + } + } + + // Set caller reference if not provided + callerReference := config.CallerReference.ValueString() + if callerReference == "" { + callerReference = id.UniqueId() + } + + // Set default timeout if not provided + timeout := 900 * time.Second + if !config.Timeout.IsNull() { + timeout = time.Duration(config.Timeout.ValueInt64()) * time.Second + } + + tflog.Info(ctx, "Starting CloudFront cache invalidation action", map[string]any{ + "distribution_id": distributionID, + "paths": paths, + "caller_reference": callerReference, + names.AttrTimeout: timeout.String(), + }) + + // Send initial progress update + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Starting cache invalidation for CloudFront distribution %s...", distributionID), + }) + + // Check if distribution exists first + _, err := findDistributionByID(ctx, conn, distributionID) + if err != nil { + if tfawserr.ErrCodeEquals(err, "NoSuchDistribution") { + resp.Diagnostics.AddError( + "Distribution Not Found", + fmt.Sprintf("CloudFront distribution %s was not found", distributionID), + ) + return + } + resp.Diagnostics.AddError( + "Failed to Describe Distribution", + fmt.Sprintf("Could not describe CloudFront distribution %s: %s", distributionID, err), + ) + return + } + + // Create invalidation request + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Creating invalidation request for %d path(s)...", len(paths)), + }) + + invalidationInput := &cloudfront.CreateInvalidationInput{ + DistributionId: aws.String(distributionID), + InvalidationBatch: &awstypes.InvalidationBatch{ + CallerReference: aws.String(callerReference), + Paths: &awstypes.Paths{ + Quantity: aws.Int32(int32(len(paths))), + Items: paths, + }, + }, + } + + output, err := conn.CreateInvalidation(ctx, invalidationInput) + if err != nil { + if tfawserr.ErrCodeEquals(err, "TooManyInvalidationsInProgress") { + resp.Diagnostics.AddError( + "Too Many Invalidations In Progress", + fmt.Sprintf("CloudFront distribution %s has too many invalidations in progress. Please wait and try again.", distributionID), + ) + return + } + if tfawserr.ErrCodeEquals(err, "InvalidArgument") { + resp.Diagnostics.AddError( + "Invalid Invalidation Request", + fmt.Sprintf("Invalid invalidation request for distribution %s: %s", distributionID, err), + ) + return + } + resp.Diagnostics.AddError( + "Failed to Create Invalidation", + fmt.Sprintf("Could not create invalidation for CloudFront distribution %s: %s", distributionID, err), + ) + return + } + + invalidationID := aws.ToString(output.Invalidation.Id) + + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Invalidation %s created, waiting for completion...", invalidationID), + }) + + // Wait for invalidation to complete with periodic progress updates + err = a.waitForInvalidationComplete(ctx, conn, distributionID, invalidationID, timeout, resp) + if err != nil { + resp.Diagnostics.AddError( + "Timeout Waiting for Invalidation to Complete", + fmt.Sprintf("CloudFront invalidation %s did not complete within %s: %s", invalidationID, timeout, err), + ) + return + } + + // Final success message + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("CloudFront cache invalidation %s completed successfully for distribution %s", invalidationID, distributionID), + }) + + tflog.Info(ctx, "CloudFront invalidate cache action completed successfully", map[string]any{ + "distribution_id": distributionID, + "invalidation_id": invalidationID, + "paths": paths, + }) +} + +// waitForInvalidationComplete waits for an invalidation to complete with progress updates +func (a *createInvalidationAction) waitForInvalidationComplete(ctx context.Context, conn *cloudfront.Client, distributionID, invalidationID string, timeout time.Duration, resp *action.InvokeResponse) error { + const ( + pollInterval = 30 * time.Second + progressInterval = 60 * time.Second + ) + + deadline := time.Now().Add(timeout) + lastProgressUpdate := time.Now() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // Check if we've exceeded the timeout + if time.Now().After(deadline) { + return fmt.Errorf("timeout after %s", timeout) + } + + // Get current invalidation status + input := &cloudfront.GetInvalidationInput{ + DistributionId: aws.String(distributionID), + Id: aws.String(invalidationID), + } + + output, err := conn.GetInvalidation(ctx, input) + if err != nil { + return fmt.Errorf("getting invalidation status: %w", err) + } + + currentStatus := aws.ToString(output.Invalidation.Status) + + // Send progress update every 60 seconds + if time.Since(lastProgressUpdate) >= progressInterval { + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Invalidation %s is currently '%s', continuing to wait for completion...", invalidationID, currentStatus), + }) + lastProgressUpdate = time.Now() + } + + // Check if we've reached completion + if aws.ToString(output.Invalidation.Status) == "Completed" { + return nil + } + + // Check if we're in an unexpected state + validStatuses := []string{ + "InProgress", + } + if !slices.Contains(validStatuses, currentStatus) && currentStatus != "Completed" { + return fmt.Errorf("invalidation entered unexpected status: %s", currentStatus) + } + + // Wait before next poll + time.Sleep(pollInterval) + } +} diff --git a/internal/service/cloudfront/create_invalidation_action_test.go b/internal/service/cloudfront/create_invalidation_action_test.go new file mode 100644 index 000000000000..db37d7f7c46e --- /dev/null +++ b/internal/service/cloudfront/create_invalidation_action_test.go @@ -0,0 +1,207 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudfront_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccCloudFrontCreateInvalidationAction_basic(t *testing.T) { + ctx := acctest.Context(t) + var distribution awstypes.Distribution + resourceName := "aws_cloudfront_distribution.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.CloudFrontEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFrontServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: testAccCheckDistributionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCreateInvalidationActionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDistributionExists(ctx, resourceName, &distribution), + testAccCheckInvalidationExists(ctx, &distribution, []string{"/*"}), + ), + }, + }, + }) +} + +// Helper: Check invalidation exists and is completed +func testAccCheckInvalidationExists(ctx context.Context, distribution *awstypes.Distribution, expectedPaths []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if distribution == nil || distribution.Id == nil { + return fmt.Errorf("Distribution is nil or has no ID") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFrontClient(ctx) + + // List invalidations for this distribution + listInput := &cloudfront.ListInvalidationsInput{ + DistributionId: distribution.Id, + } + out, err := conn.ListInvalidations(ctx, listInput) + if err != nil { + return fmt.Errorf("failed to list invalidations: %w", err) + } + + if len(out.InvalidationList.Items) == 0 { + return fmt.Errorf("no invalidations found for distribution %s", *distribution.Id) + } + + // Get the most recent invalidation + latest := out.InvalidationList.Items[0] + + // Get invalidation details + getInput := &cloudfront.GetInvalidationInput{ + DistributionId: distribution.Id, + Id: latest.Id, + } + getOut, err := conn.GetInvalidation(ctx, getInput) + if err != nil { + return fmt.Errorf("failed to get invalidation %s: %w", *latest.Id, err) + } + + invalidation := getOut.Invalidation + + // Check that the invalidation contains the expected paths + if invalidation.InvalidationBatch == nil || invalidation.InvalidationBatch.Paths == nil { + return fmt.Errorf("invalidation batch or paths is nil") + } + + actualPaths := invalidation.InvalidationBatch.Paths.Items + if len(actualPaths) != len(expectedPaths) { + return fmt.Errorf("expected %d paths, got %d", len(expectedPaths), len(actualPaths)) + } + + // Create a map for easy lookup + pathMap := make(map[string]bool) + for _, path := range actualPaths { + pathMap[path] = true + } + + // Check each expected path exists + for _, expectedPath := range expectedPaths { + if !pathMap[expectedPath] { + return fmt.Errorf("expected path %s not found in invalidation", expectedPath) + } + } + + // Wait for invalidation to complete (with timeout) + maxAttempts := 60 // 10 minutes at 10-second intervals + for attempt := range maxAttempts { + statusInput := &cloudfront.GetInvalidationInput{ + DistributionId: distribution.Id, + Id: latest.Id, + } + statusOut, err := conn.GetInvalidation(ctx, statusInput) + if err != nil { + return fmt.Errorf("failed to check invalidation status: %w", err) + } + + if *statusOut.Invalidation.Status == "Completed" { + return nil + } + + if attempt < maxAttempts-1 { + time.Sleep(10 * time.Second) + } + } + + return fmt.Errorf("invalidation %s did not complete within timeout", *latest.Id) + } +} + +// Terraform configuration with action trigger +func testAccCreateInvalidationActionConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_cloudfront_distribution" "test" { + # Use faster settings for testing + enabled = true + wait_for_deployment = false + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "test" + viewer_protocol_policy = "allow-all" + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + + min_ttl = 0 + default_ttl = 0 + max_ttl = 0 + } + + origin { + domain_name = "www.example.com" + origin_id = "test" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } + + tags = { + Name = %[1]q + } +} + +action "aws_cloudfront_create_invalidation" "test" { + config { + distribution_id = aws_cloudfront_distribution.test.id + paths = ["/*"] + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_cloudfront_create_invalidation.test] + } + } +} +`, rName) +} diff --git a/internal/service/cloudfront/function_test.go b/internal/service/cloudfront/function_test.go index 2bfd28ee204b..6e5685f669e8 100644 --- a/internal/service/cloudfront/function_test.go +++ b/internal/service/cloudfront/function_test.go @@ -27,6 +27,7 @@ func init() { func testAccErrorCheckSkipFunction(t *testing.T) resource.ErrorCheckFunc { return acctest.ErrorCheckSkipMessagesContaining(t, "InvalidParameterValueException: Unsupported source arn", + "AccessDenied", ) } diff --git a/internal/service/cloudfront/service_package_gen.go b/internal/service/cloudfront/service_package_gen.go index cc54f5f4075f..1db1f6705adc 100644 --- a/internal/service/cloudfront/service_package_gen.go +++ b/internal/service/cloudfront/service_package_gen.go @@ -17,6 +17,17 @@ import ( type servicePackage struct{} +func (p *servicePackage) Actions(ctx context.Context) []*inttypes.ServicePackageAction { + return []*inttypes.ServicePackageAction{ + { + Factory: newCreateInvalidationAction, + TypeName: "aws_cloudfront_create_invalidation", + Name: "Create Invalidation", + Region: unique.Make(inttypes.ResourceRegionDisabled()), + }, + } +} + func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.ServicePackageFrameworkDataSource { return []*inttypes.ServicePackageFrameworkDataSource{ { diff --git a/website/docs/actions/cloudfront_create_invalidation.html.markdown b/website/docs/actions/cloudfront_create_invalidation.html.markdown new file mode 100644 index 000000000000..12d86e0a1024 --- /dev/null +++ b/website/docs/actions/cloudfront_create_invalidation.html.markdown @@ -0,0 +1,135 @@ +--- +subcategory: "CloudFront" +layout: "aws" +page_title: "AWS: aws_cloudfront_create_invalidation" +description: |- + Invalidates CloudFront distribution cache for specified paths. +--- + +# Action: aws_cloudfront_create_invalidation + +~> **Note:** `aws_cloudfront_create_invalidation` is in beta. Its interface and behavior may change as the feature evolves, and breaking changes are possible. It is offered as a technical preview without compatibility guarantees until Terraform 1.14 is generally available. + +Invalidates CloudFront distribution cache for specified paths. This action creates an invalidation request and waits for it to complete. + +For information about CloudFront cache invalidation, see the [Amazon CloudFront Developer Guide](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html). For specific information about creating invalidation requests, see the [CreateInvalidation](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateInvalidation.html) page in the Amazon CloudFront API Reference. + +~> **Note:** CloudFront invalidation requests can take several minutes to complete. This action will wait for the invalidation to finish before continuing. You can only have a limited number of invalidation requests in progress at any given time. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_cloudfront_distribution" "example" { + # ... distribution configuration +} + +action "aws_cloudfront_create_invalidation" "example" { + config { + distribution_id = aws_cloudfront_distribution.example.id + paths = ["/*"] + } +} + +resource "terraform_data" "example" { + input = "trigger-invalidation" + + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_cloudfront_create_invalidation.example] + } + } +} +``` + +### Invalidate Specific Paths + +```terraform +action "aws_cloudfront_create_invalidation" "assets" { + config { + distribution_id = aws_cloudfront_distribution.example.id + paths = [ + "/images/*", + "/css/*", + "/js/app.js", + "/index.html" + ] + timeout = 1200 # 20 minutes + } +} +``` + +### With Custom Caller Reference + +```terraform +action "aws_cloudfront_create_invalidation" "deployment" { + config { + distribution_id = aws_cloudfront_distribution.example.id + paths = ["/*"] + caller_reference = "deployment-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" + timeout = 900 + } +} +``` + +### CI/CD Pipeline Integration + +Use this action in your deployment pipeline to invalidate cache after updating static assets: + +```terraform +# Trigger invalidation after S3 sync +resource "terraform_data" "deploy_complete" { + input = local.deployment_id + + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_cloudfront_create_invalidation.post_deploy] + } + } + + depends_on = [aws_s3_object.assets] +} + +action "aws_cloudfront_create_invalidation" "post_deploy" { + config { + distribution_id = aws_cloudfront_distribution.main.id + paths = [ + "/index.html", + "/manifest.json", + "/static/js/*", + "/static/css/*" + ] + } +} +``` + +### Environment-Specific Invalidation + +```terraform +locals { + cache_paths = var.environment == "production" ? [ + "/api/*", + "/assets/*" + ] : ["/*"] +} + +action "aws_cloudfront_create_invalidation" "env_specific" { + config { + distribution_id = aws_cloudfront_distribution.app.id + paths = local.cache_paths + timeout = var.environment == "production" ? 1800 : 900 + } +} +``` + +## Argument Reference + +This action supports the following arguments: + +* `distribution_id` - (Required) ID of the CloudFront distribution to invalidate cache for. Must be a valid CloudFront distribution ID (e.g., E1GHKQ2EXAMPLE). +* `paths` - (Required) List of file paths or patterns to invalidate. Use `/*` to invalidate all files. Supports specific files (`/index.html`), directory wildcards (`/images/*`), or all files (`/*`). Maximum of 3000 paths per invalidation request. Note: The first 1,000 invalidation paths per month are free, additional paths are charged per path. +* `caller_reference` - (Optional) Unique identifier for the invalidation request. If not provided, one will be generated automatically. Maximum length of 128 characters. +* `timeout` - (Optional) Timeout in seconds to wait for the invalidation to complete. Defaults to 900 seconds (15 minutes). Must be between 60 and 3600 seconds. Invalidation requests typically take 5-15 minutes to process.