opentofu/builtin/providers/aws/resource_aws_cloudfront_distribution.go
Krzysztof Wilczynski 70a90cc1f4 Handle EC2 tags related errors in CloudFront Distribution resource. (#9298)
This commits changes the behaviour in a case there was an error while
interacting with EC2 tags related to the CloudFormation Distribution
resource, fixing the issue with nil pointer dereference when despite
an error being present code path to handle tags was executed.

Also, a small re-factor of the `validateHTTP` helper method,
and a unit test added for it.

Signed-off-by: Krzysztof Wilczynski <krzysztof.wilczynski@linux.com>
2016-10-09 20:51:16 +02:00

672 lines
18 KiB
Go

package aws
import (
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudfront"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsCloudFrontDistribution() *schema.Resource {
return &schema.Resource{
Create: resourceAwsCloudFrontDistributionCreate,
Read: resourceAwsCloudFrontDistributionRead,
Update: resourceAwsCloudFrontDistributionUpdate,
Delete: resourceAwsCloudFrontDistributionDelete,
Importer: &schema.ResourceImporter{
State: resourceAwsCloudFrontDistributionImport,
},
Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"aliases": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: aliasesHash,
},
"cache_behavior": {
Type: schema.TypeSet,
Optional: true,
Set: cacheBehaviorHash,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"allowed_methods": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"cached_methods": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"compress": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"default_ttl": {
Type: schema.TypeInt,
Required: true,
},
"forwarded_values": {
Type: schema.TypeSet,
Required: true,
Set: forwardedValuesHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cookies": {
Type: schema.TypeSet,
Required: true,
Set: cookiePreferenceHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"forward": {
Type: schema.TypeString,
Required: true,
},
"whitelisted_names": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"headers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"query_string": {
Type: schema.TypeBool,
Required: true,
},
"query_string_cache_keys": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"max_ttl": {
Type: schema.TypeInt,
Required: true,
},
"min_ttl": {
Type: schema.TypeInt,
Required: true,
},
"path_pattern": {
Type: schema.TypeString,
Required: true,
},
"smooth_streaming": {
Type: schema.TypeBool,
Optional: true,
},
"target_origin_id": {
Type: schema.TypeString,
Required: true,
},
"trusted_signers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"viewer_protocol_policy": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"comment": {
Type: schema.TypeString,
Optional: true,
},
"custom_error_response": {
Type: schema.TypeSet,
Optional: true,
Set: customErrorResponseHash,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"error_caching_min_ttl": {
Type: schema.TypeInt,
Optional: true,
},
"error_code": {
Type: schema.TypeInt,
Required: true,
},
"response_code": {
Type: schema.TypeInt,
Optional: true,
},
"response_page_path": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"default_cache_behavior": {
Type: schema.TypeSet,
Required: true,
Set: defaultCacheBehaviorHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"allowed_methods": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"cached_methods": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"compress": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"default_ttl": {
Type: schema.TypeInt,
Required: true,
},
"forwarded_values": {
Type: schema.TypeSet,
Required: true,
Set: forwardedValuesHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cookies": {
Type: schema.TypeSet,
Optional: true,
Set: cookiePreferenceHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"forward": {
Type: schema.TypeString,
Required: true,
},
"whitelisted_names": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"headers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"query_string": {
Type: schema.TypeBool,
Required: true,
},
"query_string_cache_keys": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"max_ttl": {
Type: schema.TypeInt,
Required: true,
},
"min_ttl": {
Type: schema.TypeInt,
Required: true,
},
"smooth_streaming": {
Type: schema.TypeBool,
Optional: true,
},
"target_origin_id": {
Type: schema.TypeString,
Required: true,
},
"trusted_signers": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"viewer_protocol_policy": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"default_root_object": {
Type: schema.TypeString,
Optional: true,
},
"enabled": {
Type: schema.TypeBool,
Required: true,
},
"http_version": {
Type: schema.TypeString,
Optional: true,
Default: "http2",
ValidateFunc: validateHTTP,
},
"logging_config": {
Type: schema.TypeSet,
Optional: true,
Set: loggingConfigHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"bucket": {
Type: schema.TypeString,
Required: true,
},
"include_cookies": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"prefix": {
Type: schema.TypeString,
Optional: true,
Default: "",
},
},
},
},
"origin": {
Type: schema.TypeSet,
Required: true,
Set: originHash,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"custom_origin_config": {
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"origin.s3_origin_config"},
Set: customOriginConfigHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"http_port": {
Type: schema.TypeInt,
Required: true,
},
"https_port": {
Type: schema.TypeInt,
Required: true,
},
"origin_protocol_policy": {
Type: schema.TypeString,
Required: true,
},
"origin_ssl_protocols": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
"domain_name": {
Type: schema.TypeString,
Required: true,
},
"custom_header": {
Type: schema.TypeSet,
Optional: true,
Set: originCustomHeaderHash,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"value": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"origin_id": {
Type: schema.TypeString,
Required: true,
},
"origin_path": {
Type: schema.TypeString,
Optional: true,
},
"s3_origin_config": {
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"origin.custom_origin_config"},
Set: s3OriginConfigHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"origin_access_identity": {
Type: schema.TypeString,
Required: true,
},
},
},
},
},
},
},
"price_class": {
Type: schema.TypeString,
Optional: true,
Default: "PriceClass_All",
},
"restrictions": {
Type: schema.TypeSet,
Required: true,
Set: restrictionsHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"geo_restriction": {
Type: schema.TypeSet,
Required: true,
Set: geoRestrictionHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"locations": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"restriction_type": {
Type: schema.TypeString,
Required: true,
},
},
},
},
},
},
},
"viewer_certificate": {
Type: schema.TypeSet,
Required: true,
Set: viewerCertificateHash,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"acm_certificate_arn": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"viewer_certificate.cloudfront_default_certificate", "viewer_certificate.iam_certificate_id"},
},
"cloudfront_default_certificate": {
Type: schema.TypeBool,
Optional: true,
ConflictsWith: []string{"viewer_certificate.acm_certificate_arn", "viewer_certificate.iam_certificate_id"},
},
"iam_certificate_id": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"viewer_certificate.acm_certificate_arn", "viewer_certificate.cloudfront_default_certificate"},
},
"minimum_protocol_version": {
Type: schema.TypeString,
Optional: true,
Default: "SSLv3",
},
"ssl_support_method": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"web_acl_id": {
Type: schema.TypeString,
Optional: true,
},
"caller_reference": {
Type: schema.TypeString,
Computed: true,
},
"status": {
Type: schema.TypeString,
Computed: true,
},
"active_trusted_signers": {
Type: schema.TypeMap,
Computed: true,
},
"domain_name": {
Type: schema.TypeString,
Computed: true,
},
"last_modified_time": {
Type: schema.TypeString,
Computed: true,
},
"in_progress_validation_batches": {
Type: schema.TypeInt,
Computed: true,
},
"etag": {
Type: schema.TypeString,
Computed: true,
},
"hosted_zone_id": {
Type: schema.TypeString,
Computed: true,
},
// retain_on_delete is a non-API attribute that may help facilitate speedy
// deletion of a resoruce. It's mainly here for testing purposes, so
// enable at your own risk.
"retain_on_delete": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"tags": tagsSchema(),
},
}
}
func resourceAwsCloudFrontDistributionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudfrontconn
params := &cloudfront.CreateDistributionWithTagsInput{
DistributionConfigWithTags: &cloudfront.DistributionConfigWithTags{
DistributionConfig: expandDistributionConfig(d),
Tags: tagsFromMapCloudFront(d.Get("tags").(map[string]interface{})),
},
}
resp, err := conn.CreateDistributionWithTags(params)
if err != nil {
return err
}
d.SetId(*resp.Distribution.Id)
return resourceAwsCloudFrontDistributionRead(d, meta)
}
func resourceAwsCloudFrontDistributionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudfrontconn
params := &cloudfront.GetDistributionInput{
Id: aws.String(d.Id()),
}
resp, err := conn.GetDistribution(params)
if err != nil {
return err
}
// Update attributes from DistributionConfig
err = flattenDistributionConfig(d, resp.Distribution.DistributionConfig)
if err != nil {
return err
}
// Update other attributes outside of DistributionConfig
d.SetId(*resp.Distribution.Id)
err = d.Set("active_trusted_signers", flattenActiveTrustedSigners(resp.Distribution.ActiveTrustedSigners))
if err != nil {
return err
}
d.Set("status", resp.Distribution.Status)
d.Set("domain_name", resp.Distribution.DomainName)
d.Set("last_modified_time", aws.String(resp.Distribution.LastModifiedTime.String()))
d.Set("in_progress_validation_batches", resp.Distribution.InProgressInvalidationBatches)
d.Set("etag", resp.ETag)
d.Set("arn", resp.Distribution.ARN)
tagResp, err := conn.ListTagsForResource(&cloudfront.ListTagsForResourceInput{
Resource: aws.String(d.Get("arn").(string)),
})
if err != nil {
return errwrap.Wrapf(fmt.Sprintf(
"Error retrieving EC2 tags for CloudFront Distribution %q (ARN: %q): {{err}}",
d.Id(), d.Get("arn").(string)), err)
}
if err := d.Set("tags", tagsToMapCloudFront(tagResp.Tags)); err != nil {
return err
}
return nil
}
func resourceAwsCloudFrontDistributionUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudfrontconn
params := &cloudfront.UpdateDistributionInput{
Id: aws.String(d.Id()),
DistributionConfig: expandDistributionConfig(d),
IfMatch: aws.String(d.Get("etag").(string)),
}
_, err := conn.UpdateDistribution(params)
if err != nil {
return err
}
if err := setTagsCloudFront(conn, d, d.Get("arn").(string)); err != nil {
return err
}
return resourceAwsCloudFrontDistributionRead(d, meta)
}
func resourceAwsCloudFrontDistributionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).cloudfrontconn
// manually disable the distribution first
d.Set("enabled", false)
err := resourceAwsCloudFrontDistributionUpdate(d, meta)
if err != nil {
return err
}
// skip delete if retain_on_delete is enabled
if d.Get("retain_on_delete").(bool) {
log.Printf("[WARN] Removing CloudFront Distribution ID %q with `retain_on_delete` set. Please delete this distribution manually.", d.Id())
d.SetId("")
return nil
}
// Distribution needs to be in deployed state again before it can be deleted.
err = resourceAwsCloudFrontDistributionWaitUntilDeployed(d.Id(), meta)
if err != nil {
return err
}
// now delete
params := &cloudfront.DeleteDistributionInput{
Id: aws.String(d.Id()),
IfMatch: aws.String(d.Get("etag").(string)),
}
_, err = conn.DeleteDistribution(params)
if err != nil {
return err
}
// Done
d.SetId("")
return nil
}
// resourceAwsCloudFrontWebDistributionWaitUntilDeployed blocks until the
// distribution is deployed. It currently takes exactly 15 minutes to deploy
// but that might change in the future.
func resourceAwsCloudFrontDistributionWaitUntilDeployed(id string, meta interface{}) error {
stateConf := &resource.StateChangeConf{
Pending: []string{"InProgress", "Deployed"},
Target: []string{"Deployed"},
Refresh: resourceAwsCloudFrontWebDistributionStateRefreshFunc(id, meta),
Timeout: 40 * time.Minute,
MinTimeout: 15 * time.Second,
Delay: 10 * time.Minute,
}
_, err := stateConf.WaitForState()
return err
}
// The refresh function for resourceAwsCloudFrontWebDistributionWaitUntilDeployed.
func resourceAwsCloudFrontWebDistributionStateRefreshFunc(id string, meta interface{}) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
conn := meta.(*AWSClient).cloudfrontconn
params := &cloudfront.GetDistributionInput{
Id: aws.String(id),
}
resp, err := conn.GetDistribution(params)
if err != nil {
log.Printf("[WARN] Error retrieving CloudFront Distribution %q details: %s", id, err)
return nil, "", err
}
if resp == nil {
return nil, "", nil
}
return resp.Distribution, *resp.Distribution.Status, nil
}
}
// validateHTTP ensures that the http_version resource parameter is
// correct.
func validateHTTP(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if value != "http1.1" && value != "http2" {
errors = append(errors, fmt.Errorf(
"%q contains an invalid HTTP version parameter %q. Valid parameters are either %q or %q.",
k, value, "http1.1", "http2"))
}
return
}