-
Notifications
You must be signed in to change notification settings - Fork 9.8k
New action: aws_cloudfront_create_invalidation
#43955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
cc24c5e
New action: aws_cloudfront_create_invalidation
YakDriver 864ee42
Add actions docs
YakDriver 6aaaac7
Add action reg
YakDriver 33a88b6
Add test
YakDriver 9b2f1a9
Add changelog
YakDriver ab52424
Add docs warning
YakDriver 6d60e0a
Update docs
YakDriver 7cbd313
Update action for simplification
YakDriver 1c43cb1
Update warning ntoe
YakDriver c993011
Update/fix docs
YakDriver a2cf487
Remove unnecessary part of selector
YakDriver bdaaeb0
Merge branch 'f-ec2-instance-action' into f-cloudfront-invalidate-cac…
YakDriver d261f65
Merge branch 'f-ec2-instance-action' into f-cloudfront-invalidate-cac…
YakDriver c7409e3
Merge branch 'f-ec2-instance-action' into f-cloudfront-invalidate-cac…
YakDriver 9a70a5a
User newer terraform-plugin-testing
YakDriver 459bb3f
Add test guard by TF version
YakDriver 3af3a17
Add skip for access denied
YakDriver 1fa89e2
Merge remote-tracking branch 'origin/main' into f-cloudfront-invalida…
YakDriver 361a6cc
Move note higher
YakDriver 2374a35
Move note lil lower
YakDriver File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ```release-note:new-action | ||
| aws_cloudfront_create_invalidation | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
300 changes: 300 additions & 0 deletions
300
internal/service/cloudfront/create_invalidation_action.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker, but may be nice to write a custom type to handle this validation in the future.