diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b16b76eff..f85f8e3844 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -88,6 +88,7 @@ Ansible Changes By Release
* aws_kms_facts
* aws_s3_cors
* aws_ssm_parameter_store
+ * cloudfront_distribution
* ec2_ami_facts
* ec2_asg_lifecycle_hook
* ec2_placement_group
diff --git a/hacking/aws_config/testing_policies/cloudfront-policy.json b/hacking/aws_config/testing_policies/cloudfront-policy.json
new file mode 100644
index 0000000000..5bb22ffe5a
--- /dev/null
+++ b/hacking/aws_config/testing_policies/cloudfront-policy.json
@@ -0,0 +1,28 @@
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AllowCloudfrontUsage",
+ "Effect": "Allow",
+ "Action": [
+ "cloudfront:CreateDistribution",
+ "cloudfront:CreateDistributionWithTags",
+ "cloudfront:DeleteDistribution",
+ "cloudfront:GetDistribution",
+ "cloudfront:GetStreamingDistribution",
+ "cloudfront:GetDistributionConfig",
+ "cloudfront:GetStreamingDistributionConfig",
+ "cloudfront:GetInvalidation",
+ "cloudfront:ListDistributions",
+ "cloudfront:ListDistributionsByWebACLId",
+ "cloudfront:ListInvalidations",
+ "cloudfront:ListStreamingDistributions",
+ "cloudfront:ListTagsForResource",
+ "cloudfront:TagResource",
+ "cloudfront:UntagResource",
+ "cloudfront:UpdateDistribution"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
diff --git a/lib/ansible/module_utils/aws/cloudfront_facts.py b/lib/ansible/module_utils/aws/cloudfront_facts.py
new file mode 100644
index 0000000000..37b41de159
--- /dev/null
+++ b/lib/ansible/module_utils/aws/cloudfront_facts.py
@@ -0,0 +1,234 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017 Willem van Ketwich
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see .
+#
+# Author:
+# - Willem van Ketwich
+#
+# Common functionality to be used by the modules:
+# - cloudfront_distribution
+# - cloudfront_invalidation
+# - cloudfront_origin_access_identity
+
+
+"""
+Common cloudfront facts shared between modules
+"""
+
+from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn
+from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict
+
+try:
+ import botocore
+except ImportError:
+ pass
+
+
+class CloudFrontFactsServiceManager(object):
+ """Handles CloudFront Facts Services"""
+
+ def __init__(self, module):
+ self.module = module
+
+ region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
+ self.client = boto3_conn(module, conn_type='client',
+ resource='cloudfront', region=region,
+ endpoint=ec2_url, **aws_connect_kwargs)
+
+ def get_distribution(self, distribution_id):
+ try:
+ return self.client.get_distribution(Id=distribution_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing distribution")
+
+ def get_distribution_config(self, distribution_id):
+ try:
+ return self.client.get_distribution_config(Id=distribution_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing distribution configuration")
+
+ def get_origin_access_identity(self, origin_access_identity_id):
+ try:
+ return self.client.get_cloud_front_origin_access_identity(Id=origin_access_identity_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing origin access identity")
+
+ def get_origin_access_identity_config(self, origin_access_identity_id):
+ try:
+ return self.client.get_cloud_front_origin_access_identity_config(Id=origin_access_identity_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing origin access identity configuration")
+
+ def get_invalidation(self, distribution_id, invalidation_id):
+ try:
+ return self.client.get_invalidation(DistributionId=distribution_id, Id=invalidation_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing invalidation")
+
+ def get_streaming_distribution(self, distribution_id):
+ try:
+ return self.client.get_streaming_distribution(Id=distribution_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing streaming distribution")
+
+ def get_streaming_distribution_config(self, distribution_id):
+ try:
+ return self.client.get_streaming_distribution_config(Id=distribution_id)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error describing streaming distribution")
+
+ def list_origin_access_identities(self):
+ try:
+ paginator = self.client.get_paginator('list_cloud_front_origin_access_identities')
+ result = paginator.paginate().build_full_result()['CloudFrontOriginAccessIdentityList']
+ return result.get('Items', [])
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error listing cloud front origin access identities")
+
+ def list_distributions(self, keyed=True):
+ try:
+ paginator = self.client.get_paginator('list_distributions')
+ result = paginator.paginate().build_full_result().get('DistributionList', {})
+ distribution_list = result.get('Items', [])
+ if not keyed:
+ return distribution_list
+ return self.keyed_list_helper(distribution_list)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error listing distributions")
+
+ def list_distributions_by_web_acl_id(self, web_acl_id):
+ try:
+ result = self.client.list_distributions_by_web_acl_id(WebAclId=web_acl_id)
+ distribution_list = result.get('DistributionList', {}).get('Items', [])
+ return self.keyed_list_helper(distribution_list)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error listing distributions by web acl id")
+
+ def list_invalidations(self, distribution_id):
+ try:
+ paginator = self.client.get_paginator('list_invalidations')
+ result = paginator.paginate(DistributionId=distribution_id).build_full_result()
+ return result.get('InvalidationList', {}).get('Items', [])
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error listing invalidations")
+
+ def list_streaming_distributions(self, keyed=True):
+ try:
+ paginator = self.client.get_paginator('list_streaming_distributions')
+ result = paginator.paginate().build_full_result()
+ streaming_distribution_list = result.get('StreamingDistributionList', {}).get('Items', [])
+ if not keyed:
+ return streaming_distribution_list
+ return self.keyed_list_helper(streaming_distribution_list)
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error listing streaming distributions")
+
+ def summary(self):
+ summary_dict = {}
+ summary_dict.update(self.summary_get_distribution_list(False))
+ summary_dict.update(self.summary_get_distribution_list(True))
+ summary_dict.update(self.summary_get_origin_access_identity_list())
+ return summary_dict
+
+ def summary_get_origin_access_identity_list(self):
+ try:
+ origin_access_identity_list = {'origin_access_identities': []}
+ origin_access_identities = self.list_origin_access_identities()
+ for origin_access_identity in origin_access_identities:
+ oai_id = origin_access_identity['Id']
+ oai_full_response = self.get_origin_access_identity(oai_id)
+ oai_summary = {'Id': oai_id, 'ETag': oai_full_response['ETag']}
+ origin_access_identity_list['origin_access_identities'].append(oai_summary)
+ return origin_access_identity_list
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error generating summary of origin access identities")
+
+ def summary_get_distribution_list(self, streaming=False):
+ try:
+ list_name = 'streaming_distributions' if streaming else 'distributions'
+ key_list = ['Id', 'ARN', 'Status', 'LastModifiedTime', 'DomainName', 'Comment', 'PriceClass', 'Enabled']
+ distribution_list = {list_name: []}
+ distributions = self.list_streaming_distributions(False) if streaming else self.list_distributions(False)
+ for dist in distributions:
+ temp_distribution = {}
+ for key_name in key_list:
+ temp_distribution[key_name] = dist[key_name]
+ temp_distribution['Aliases'] = [alias for alias in dist['Aliases'].get('Items', [])]
+ temp_distribution['ETag'] = self.get_etag_from_distribution_id(dist['Id'], streaming)
+ if not streaming:
+ temp_distribution['WebACLId'] = dist['WebACLId']
+ invalidation_ids = self.get_list_of_invalidation_ids_from_distribution_id(dist['Id'])
+ if invalidation_ids:
+ temp_distribution['Invalidations'] = invalidation_ids
+ resource_tags = self.client.list_tags_for_resource(Resource=dist['ARN'])
+ temp_distribution['Tags'] = boto3_tag_list_to_ansible_dict(resource_tags['Tags'].get('Items', []))
+ distribution_list[list_name].append(temp_distribution)
+ return distribution_list
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error generating summary of distributions")
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error generating summary of distributions")
+
+ def get_etag_from_distribution_id(self, distribution_id, streaming):
+ distribution = {}
+ if not streaming:
+ distribution = self.get_distribution(distribution_id)
+ else:
+ distribution = self.get_streaming_distribution(distribution_id)
+ return distribution['ETag']
+
+ def get_list_of_invalidation_ids_from_distribution_id(self, distribution_id):
+ try:
+ invalidation_ids = []
+ invalidations = self.list_invalidations(distribution_id)
+ for invalidation in invalidations:
+ invalidation_ids.append(invalidation['Id'])
+ return invalidation_ids
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error getting list of invalidation ids")
+
+ def get_distribution_id_from_domain_name(self, domain_name):
+ try:
+ distribution_id = ""
+ distributions = self.list_distributions(False)
+ distributions += self.list_streaming_distributions(False)
+ for dist in distributions:
+ if 'Items' in dist['Aliases']:
+ for alias in dist['Aliases']['Items']:
+ if str(alias).lower() == domain_name.lower():
+ distribution_id = dist['Id']
+ break
+ return distribution_id
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error getting distribution id from domain name")
+
+ def get_aliases_from_distribution_id(self, distribution_id):
+ try:
+ distribution = self.get_distribution(distribution_id)
+ return distribution['DistributionConfig']['Aliases'].get('Items', [])
+ except botocore.exceptions.ClientError as e:
+ self.module.fail_json_aws(e, msg="Error getting list of aliases from distribution_id")
+
+ def keyed_list_helper(self, list_to_key):
+ keyed_list = dict()
+ for item in list_to_key:
+ distribution_id = item['Id']
+ if 'Items' in item['Aliases']:
+ aliases = item['Aliases']['Items']
+ for alias in aliases:
+ keyed_list.update({alias: item})
+ keyed_list.update({distribution_id: item})
+ return keyed_list
diff --git a/lib/ansible/modules/cloud/amazon/cloudfront_distribution.py b/lib/ansible/modules/cloud/amazon/cloudfront_distribution.py
new file mode 100644
index 0000000000..51e7d16761
--- /dev/null
+++ b/lib/ansible/modules/cloud/amazon/cloudfront_distribution.py
@@ -0,0 +1,1972 @@
+#!/usr/bin/python
+# Copyright (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+---
+
+module: cloudfront_distribution
+
+short_description: create, update and delete aws cloudfront distributions.
+
+description:
+ - Allows for easy creation, updating and deletion of CloudFront distributions.
+
+requirements:
+ - boto3 >= 1.0.0
+ - python >= 2.6
+
+version_added: "2.5"
+
+author:
+ - Willem van Ketwich (@wilvk)
+ - Will Thames (@willthames)
+
+options:
+
+ state:
+ description:
+ - The desired state of the distribution
+ present - creates a new distribution or updates an existing distribution.
+ absent - deletes an existing distribution.
+ choices: ['present', 'absent']
+ default: 'present'
+
+ distribution_id:
+ description:
+ - The id of the cloudfront distribution. This parameter can be exchanged with I(alias) or I(caller_reference) and is used in conjunction with I(e_tag).
+
+ e_tag:
+ description:
+ - A unique identifier of a modified or existing distribution. Used in conjunction with I(distribution_id).
+ Is determined automatically if not specified.
+
+ caller_reference:
+ description:
+ - A unique identifier for creating and updating cloudfront distributions. Each caller reference must be unique across all distributions. e.g. a caller
+ reference used in a web distribution cannot be reused in a streaming distribution. This parameter can be used instead of I(distribution_id)
+ to reference an existing distribution. If not specified, this defaults to a datetime stamp of the format
+ 'YYYY-MM-DDTHH:MM:SS.ffffff'.
+
+ tags:
+ description:
+ - Should be input as a dict() of key-value pairs.
+ Note that numeric keys or values must be wrapped in quotes. e.g. "Priority:" '1'
+
+ purge_tags:
+ description:
+ - Specifies whether existing tags will be removed before adding new tags. When I(purge_tags=yes), existing tags are removed and I(tags) are added, if
+ specified. If no tags are specified, it removes all existing tags for the distribution. When I(purge_tags=no), existing tags are kept and I(tags)
+ are added, if specified.
+ default: 'no'
+ choices: ['yes', 'no']
+
+ alias:
+ description:
+ - The name of an alias (CNAME) that is used in a distribution. This is used to effectively reference a distribution by its alias as an alias can only
+ be used by one distribution per AWS account. This variable avoids having to provide the I(distribution_id) as well as
+ the I(e_tag), or I(caller_reference) of an existing distribution.
+
+ aliases:
+ description:
+ - A I(list[]) of domain name aliases (CNAMEs) as strings to be used for the distribution. Each alias must be unique across all distribution for the AWS
+ account.
+
+ default_root_object:
+ description:
+ - A config element that specifies the path to request when the user requests the origin. e.g. if specified as 'index.html', this maps to
+ www.example.com/index.html when www.example.com is called by the user. This prevents the entire distribution origin from being exposed at the root.
+
+ default_origin_domain_name:
+ description:
+ - The domain name to use for an origin if no I(origins) have been specified. Should only be used on a first run of generating a distribution and not on
+ subsequent runs. Should not be used in conjunction with I(distribution_id), I(caller_reference) or I(alias).
+
+ default_origin_path:
+ description:
+ - The default origin path to specify for an origin if no I(origins) have been specified. Defaults to empty if not specified.
+
+ origins:
+ description:
+ - A config element that is a I(list[]) of complex origin objects to be specified for the distribution. Used for creating and updating distributions.
+ Each origin item comprises the attributes
+ I(id)
+ I(domain_name) (defaults to default_origin_domain_name if not specified)
+ I(origin_path) (defaults to default_origin_path if not specified)
+ I(custom_headers[])
+ I(header_name)
+ I(header_value)
+ I(s3_origin_access_identity_enabled)
+ I(custom_origin_config)
+ I(http_port)
+ I(https_port)
+ I(origin_protocol_policy)
+ I(origin_ssl_protocols[])
+ I(origin_read_timeout)
+ I(origin_keepalive_timeout)
+
+ purge_origins:
+ description: Whether to remove any origins that aren't listed in I(origins)
+ default: false
+
+ default_cache_behavior:
+ description:
+ - A config element that is a complex object specifying the default cache behavior of the distribution. If not specified, the I(target_origin_id) is
+ defined as the I(target_origin_id) of the first valid I(cache_behavior) in I(cache_behaviors) with defaults.
+ The default cache behavior comprises the attributes
+ I(target_origin_id)
+ I(forwarded_values)
+ I(query_string)
+ I(cookies)
+ I(forward)
+ I(whitelisted_names)
+ I(headers[])
+ I(query_string_cache_keys[])
+ I(trusted_signers)
+ I(enabled)
+ I(items[])
+ I(viewer_protocol_policy)
+ I(min_ttl)
+ I(allowed_methods)
+ I(items[])
+ I(cached_methods[])
+ I(smooth_streaming)
+ I(default_ttl)
+ I(max_ttl)
+ I(compress)
+ I(lambda_function_associations[])
+ I(lambda_function_arn)
+ I(event_type)
+
+ cache_behaviors:
+ description:
+ - A config element that is a I(list[]) of complex cache behavior objects to be specified for the distribution. The order
+ of the list is preserved across runs unless C(purge_cache_behavior) is enabled.
+ Each cache behavior comprises the attributes
+ I(path_pattern)
+ I(target_origin_id)
+ I(forwarded_values)
+ I(query_string)
+ I(cookies)
+ I(forward)
+ I(whitelisted_names)
+ I(headers[])
+ I(query_string_cache_keys[])
+ I(trusted_signers)
+ I(enabled)
+ I(items[])
+ I(viewer_protocol_policy)
+ I(min_ttl)
+ I(allowed_methods)
+ I(items[])
+ I(cached_methods[])
+ I(smooth_streaming)
+ I(default_ttl)
+ I(max_ttl)
+ I(compress)
+ I(lambda_function_associations[])
+
+ purge_cache_behaviors:
+ description: Whether to remove any cache behaviors that aren't listed in I(cache_behaviors). This switch
+ also allows the reordering of cache_behaviors.
+ default: false
+
+ custom_error_responses:
+ description:
+ - A config element that is a I(list[]) of complex custom error responses to be specified for the distribution. This attribute configures custom http
+ error messages returned to the user.
+ Each custom error response object comprises the attributes
+ I(error_code)
+ I(reponse_page_path)
+ I(response_code)
+ I(error_caching_min_ttl)
+
+ purge_custom_error_responses:
+ description: Whether to remove any custom error responses that aren't listed in I(custom_error_responses)
+ default: false
+
+ comment:
+ description:
+ - A comment that describes the cloudfront distribution. If not specified, it defaults to a
+ generic message that it has been created with Ansible, and a datetime stamp.
+
+ logging:
+ description:
+ - A config element that is a complex object that defines logging for the distribution.
+ The logging object comprises the attributes
+ I(enabled)
+ I(include_cookies)
+ I(bucket)
+ I(prefix)
+
+ price_class:
+ description:
+ - A string that specifies the pricing class of the distribution. As per
+ U(https://aws.amazon.com/cloudfront/pricing/)
+ I(price_class=PriceClass_100) consists of the areas
+ United States
+ Canada
+ Europe
+ I(price_class=PriceClass_200) consists of the areas
+ United States
+ Canada
+ Europe
+ Hong Kong, Philippines, S. Korea, Singapore & Taiwan
+ Japan
+ India
+ I(price_class=PriceClass_All) consists of the areas
+ United States
+ Canada
+ Europe
+ Hong Kong, Philippines, S. Korea, Singapore & Taiwan
+ Japan
+ India
+ South America
+ Australia
+ choices: ['PriceClass_100', 'PriceClass_200', 'PriceClass_All']
+ default: aws defaults this to 'PriceClass_All'
+
+ enabled:
+ description:
+ - A boolean value that specifies whether the distribution is enabled or disabled.
+ default: 'yes'
+ choices: ['yes', 'no']
+
+ viewer_certificate:
+ description:
+ - A config element that is a complex object that specifies the encryption details of the distribution.
+ Comprises the following attributes
+ I(cloudfront_default_certificate)
+ I(iam_certificate_id)
+ I(acm_certificate_arn)
+ I(ssl_support_method)
+ I(minimum_protocol_version)
+ I(certificate)
+ I(certificate_source)
+
+ restrictions:
+ description:
+ - A config element that is a complex object that describes how a distribution should restrict it's content.
+ The restriction object comprises the following attributes
+ I(geo_restriction)
+ I(restriction_type)
+ I(items[])
+
+ web_acl_id:
+ description:
+ - The id of a Web Application Firewall (WAF) Access Control List (ACL).
+
+ http_version:
+ description:
+ - The version of the http protocol to use for the distribution.
+ choices: [ 'http1.1', 'http2' ]
+ default: aws defaults this to 'http2'
+
+ ipv6_enabled:
+ description:
+ - Determines whether IPv6 support is enabled or not.
+ choices: ['yes', 'no']
+ default: 'no'
+
+ wait:
+ description:
+ - Specifies whether the module waits until the distribution has completed processing the creation or update.
+ choices: ['yes', 'no']
+ default: 'no'
+
+ wait_timeout:
+ description:
+ - Specifies the duration in seconds to wait for a timeout of a cloudfront create or update. Defaults to 1800 seconds (30 minutes).
+ default: 1800
+
+'''
+
+EXAMPLES = '''
+
+# create a basic distribution with defaults and tags
+
+- cloudfront_distribution:
+ state: present
+ default_origin_domain_name: www.my-cloudfront-origin.com
+ tags:
+ Name: example distribution
+ Project: example project
+ Priority: '1'
+
+# update a distribution comment by distribution_id
+
+- cloudfront_distribution:
+ state: present
+ distribution_id: E1RP5A2MJ8073O
+ comment: modified by ansible cloudfront.py
+
+# update a distribution comment by caller_reference
+
+- cloudfront_distribution:
+ state: present
+ caller_reference: my cloudfront distribution 001
+ comment: modified by ansible cloudfront.py
+
+# update a distribution's aliases and comment using the distribution_id as a reference
+
+- cloudfront_distribution:
+ state: present
+ distribution_id: E1RP5A2MJ8073O
+ comment: modified by cloudfront.py again
+ aliases: [ 'www.my-distribution-source.com', 'zzz.aaa.io' ]
+
+# update a distribution's aliases and comment using an alias as a reference
+
+- cloudfront_distribution:
+ state: present
+ caller_reference: my test distribution
+ comment: modified by cloudfront.py again
+ aliases:
+ - www.my-distribution-source.com
+ - zzz.aaa.io
+
+# update a distribution's comment and aliases and tags and remove existing tags
+
+- cloudfront_distribution:
+ state: present
+ distribution_id: E15BU8SDCGSG57
+ comment: modified by cloudfront.py again
+ aliases:
+ - tested.com
+ tags:
+ Project: distribution 1.2
+ purge_tags: yes
+
+# create a distribution with an origin, logging and default cache behavior
+
+- cloudfront_distribution:
+ state: present
+ caller_reference: unique test distribution id
+ origins:
+ - id: 'my test origin-000111'
+ domain_name: www.example.com
+ origin_path: /production
+ custom_headers:
+ - header_name: MyCustomHeaderName
+ header_value: MyCustomHeaderValue
+ default_cache_behavior:
+ target_origin_id: 'my test origin-000111'
+ forwarded_values:
+ query_string: true
+ cookies:
+ forward: all
+ headers:
+ - '*'
+ viewer_protocol_policy: allow-all
+ smooth_streaming: true
+ compress: true
+ allowed_methods:
+ items:
+ - GET
+ - HEAD
+ cached_methods:
+ - GET
+ - HEAD
+ logging:
+ enabled: true
+ include_cookies: false
+ bucket: mylogbucket.s3.amazonaws.com
+ prefix: myprefix/
+ enabled: false
+ comment: this is a cloudfront distribution with logging
+
+# delete a distribution
+
+- cloudfront_distribution:
+ state: absent
+ caller_reference: replaceable distribution
+'''
+
+RETURN = '''
+active_trusted_signers:
+ description: Key pair IDs that CloudFront is aware of for each trusted signer
+ returned: always
+ type: complex
+ contains:
+ enabled:
+ description: Whether trusted signers are in use
+ returned: always
+ type: bool
+ sample: false
+ quantity:
+ description: Number of trusted signers
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: Number of trusted signers
+ returned: when there are trusted signers
+ type: list
+ sample:
+ - key_pair_id
+aliases:
+ description: Aliases that refer to the distribution
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of aliases
+ returned: always
+ type: list
+ sample:
+ - test.example.com
+ quantity:
+ description: Number of aliases
+ returned: always
+ type: int
+ sample: 1
+arn:
+ description: Amazon Resource Name of the distribution
+ returned: always
+ type: string
+ sample: arn:aws:cloudfront::123456789012:distribution/E1234ABCDEFGHI
+cache_behaviors:
+ description: Cloudfront cache behaviors
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of cache behaviors
+ returned: always
+ type: complex
+ contains:
+ allowed_methods:
+ description: Methods allowed by the cache behavior
+ returned: always
+ type: complex
+ contains:
+ cached_methods:
+ description: Methods cached by the cache behavior
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of cached methods
+ returned: always
+ type: list
+ sample:
+ - HEAD
+ - GET
+ quantity:
+ description: Count of cached methods
+ returned: always
+ type: int
+ sample: 2
+ items:
+ description: List of methods allowed by the cache behavior
+ returned: always
+ type: list
+ sample:
+ - HEAD
+ - GET
+ quantity:
+ description: Count of methods allowed by the cache behavior
+ returned: always
+ type: int
+ sample: 2
+ compress:
+ description: Whether compression is turned on for the cache behavior
+ returned: always
+ type: bool
+ sample: false
+ default_ttl:
+ description: Default Time to Live of the cache behavior
+ returned: always
+ type: int
+ sample: 86400
+ forwarded_values:
+ description: Values forwarded to the origin for this cache behavior
+ returned: always
+ type: complex
+ contains:
+ cookies:
+ description: Cookies to forward to the origin
+ returned: always
+ type: complex
+ contains:
+ forward:
+ description: Which cookies to forward to the origin for this cache behavior
+ returned: always
+ type: string
+ sample: none
+ whitelisted_names:
+ description: The names of the cookies to forward to the origin for this cache behavior
+ returned: when I(forward) is C(whitelist)
+ type: complex
+ contains:
+ quantity:
+ description: Count of cookies to forward
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of cookies to forward
+ returned: when list is not empty
+ type: list
+ sample: my_cookie
+ headers:
+ description: Which headers are used to vary on cache retrievals
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of headers to vary on
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of headers to vary on
+ returned: when list is not empty
+ type: list
+ sample:
+ - Host
+ query_string:
+ description: Whether the query string is used in cache lookups
+ returned: always
+ type: bool
+ sample: false
+ query_string_cache_keys:
+ description: Which query string keys to use in cache lookups
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of query string cache keys to use in cache lookups
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of query string cache keys to use in cache lookups
+ returned: when list is not empty
+ type: list
+ sample:
+ lambda_function_associations:
+ description: Lambda function associations for a cache behavior
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of lambda function associations
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of lambda function associations
+ returned: when list is not empty
+ type: list
+ sample:
+ - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
+ event_type: viewer-response
+ max_ttl:
+ description: Maximum Time to Live
+ returned: always
+ type: int
+ sample: 31536000
+ min_ttl:
+ description: Minimum Time to Live
+ returned: always
+ type: int
+ sample: 0
+ path_pattern:
+ description: Path pattern that determines this cache behavior
+ returned: always
+ type: string
+ sample: /path/to/files/*
+ smooth_streaming:
+ description: Whether smooth streaming is enabled
+ returned: always
+ type: bool
+ sample: false
+ target_origin_id:
+ description: Id of origin reference by this cache behavior
+ returned: always
+ type: string
+ sample: origin_abcd
+ trusted_signers:
+ description: Trusted signers
+ returned: always
+ type: complex
+ contains:
+ enabled:
+ description: Whether trusted signers are enabled for this cache behavior
+ returned: always
+ type: bool
+ sample: false
+ quantity:
+ description: Count of trusted signers
+ returned: always
+ type: int
+ sample: 1
+ viewer_protocol_policy:
+ description: Policy of how to handle http/https
+ returned: always
+ type: string
+ sample: redirect-to-https
+ quantity:
+ description: Count of cache behaviors
+ returned: always
+ type: int
+ sample: 1
+
+caller_reference:
+ description: Idempotency reference given when creating cloudfront distribution
+ returned: always
+ type: string
+ sample: '1484796016700'
+comment:
+ description: Any comments you want to include about the distribution
+ returned: always
+ type: string
+ sample: 'my first cloudfront distribution'
+custom_error_responses:
+ description: Custom error responses to use for error handling
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of custom error responses
+ returned: always
+ type: complex
+ contains:
+ error_caching_min_ttl:
+ description: Mininum time to cache this error response
+ returned: always
+ type: int
+ sample: 300
+ error_code:
+ description: Origin response code that triggers this error response
+ returned: always
+ type: int
+ sample: 500
+ response_code:
+ description: Response code to return to the requester
+ returned: always
+ type: string
+ sample: '500'
+ response_page_path:
+ description: Path that contains the error page to display
+ returned: always
+ type: string
+ sample: /errors/5xx.html
+ quantity:
+ description: Count of custom error response items
+ returned: always
+ type: int
+ sample: 1
+default_cache_behavior:
+ description: Default cache behavior
+ returned: always
+ type: complex
+ contains:
+ allowed_methods:
+ description: Methods allowed by the cache behavior
+ returned: always
+ type: complex
+ contains:
+ cached_methods:
+ description: Methods cached by the cache behavior
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of cached methods
+ returned: always
+ type: list
+ sample:
+ - HEAD
+ - GET
+ quantity:
+ description: Count of cached methods
+ returned: always
+ type: int
+ sample: 2
+ items:
+ description: List of methods allowed by the cache behavior
+ returned: always
+ type: list
+ sample:
+ - HEAD
+ - GET
+ quantity:
+ description: Count of methods allowed by the cache behavior
+ returned: always
+ type: int
+ sample: 2
+ compress:
+ description: Whether compression is turned on for the cache behavior
+ returned: always
+ type: bool
+ sample: false
+ default_ttl:
+ description: Default Time to Live of the cache behavior
+ returned: always
+ type: int
+ sample: 86400
+ forwarded_values:
+ description: Values forwarded to the origin for this cache behavior
+ returned: always
+ type: complex
+ contains:
+ cookies:
+ description: Cookies to forward to the origin
+ returned: always
+ type: complex
+ contains:
+ forward:
+ description: Which cookies to forward to the origin for this cache behavior
+ returned: always
+ type: string
+ sample: none
+ whitelisted_names:
+ description: The names of the cookies to forward to the origin for this cache behavior
+ returned: when I(forward) is C(whitelist)
+ type: complex
+ contains:
+ quantity:
+ description: Count of cookies to forward
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of cookies to forward
+ returned: when list is not empty
+ type: list
+ sample: my_cookie
+ headers:
+ description: Which headers are used to vary on cache retrievals
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of headers to vary on
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of headers to vary on
+ returned: when list is not empty
+ type: list
+ sample:
+ - Host
+ query_string:
+ description: Whether the query string is used in cache lookups
+ returned: always
+ type: bool
+ sample: false
+ query_string_cache_keys:
+ description: Which query string keys to use in cache lookups
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of query string cache keys to use in cache lookups
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of query string cache keys to use in cache lookups
+ returned: when list is not empty
+ type: list
+ sample:
+ lambda_function_associations:
+ description: Lambda function associations for a cache behavior
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of lambda function associations
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of lambda function associations
+ returned: when list is not empty
+ type: list
+ sample:
+ - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
+ event_type: viewer-response
+ max_ttl:
+ description: Maximum Time to Live
+ returned: always
+ type: int
+ sample: 31536000
+ min_ttl:
+ description: Minimum Time to Live
+ returned: always
+ type: int
+ sample: 0
+ path_pattern:
+ description: Path pattern that determines this cache behavior
+ returned: always
+ type: string
+ sample: /path/to/files/*
+ smooth_streaming:
+ description: Whether smooth streaming is enabled
+ returned: always
+ type: bool
+ sample: false
+ target_origin_id:
+ description: Id of origin reference by this cache behavior
+ returned: always
+ type: string
+ sample: origin_abcd
+ trusted_signers:
+ description: Trusted signers
+ returned: always
+ type: complex
+ contains:
+ enabled:
+ description: Whether trusted signers are enabled for this cache behavior
+ returned: always
+ type: bool
+ sample: false
+ quantity:
+ description: Count of trusted signers
+ returned: always
+ type: int
+ sample: 1
+ viewer_protocol_policy:
+ description: Policy of how to handle http/https
+ returned: always
+ type: string
+ sample: redirect-to-https
+default_root_object:
+ description: The object that you want CloudFront to request from your origin (for example, index.html)
+ when a viewer requests the root URL for your distribution
+ returned: always
+ type: string
+ sample: ''
+diff:
+ description: Difference between previous configuration and new configuration
+ returned: always
+ type: dict
+ sample: {}
+domain_name:
+ description: Domain name of cloudfront distribution
+ returned: always
+ type: string
+ sample: d1vz8pzgurxosf.cloudfront.net
+enabled:
+ description: Whether the cloudfront distribution is enabled or not
+ returned: always
+ type: bool
+ sample: true
+http_version:
+ description: Version of HTTP supported by the distribution
+ returned: always
+ type: string
+ sample: http2
+id:
+ description: Cloudfront distribution ID
+ returned: always
+ type: string
+ sample: E123456ABCDEFG
+in_progress_invalidation_batches:
+ description: The number of invalidation batches currently in progress
+ returned: always
+ type: int
+ sample: 0
+is_ipv6_enabled:
+ description: Whether IPv6 is enabled
+ returned: always
+ type: bool
+ sample: true
+last_modified_time:
+ description: Date and time distribution was last modified
+ returned: always
+ type: string
+ sample: '2017-10-13T01:51:12.656000+00:00'
+logging:
+ description: Logging information
+ returned: always
+ type: complex
+ contains:
+ bucket:
+ description: S3 bucket logging destination
+ returned: always
+ type: string
+ sample: logs-example-com.s3.amazonaws.com
+ enabled:
+ description: Whether logging is enabled
+ returned: always
+ type: bool
+ sample: true
+ include_cookies:
+ description: Whether to log cookies
+ returned: always
+ type: bool
+ sample: false
+ prefix:
+ description: Prefix added to logging object names
+ returned: always
+ type: string
+ sample: cloudfront/test
+origins:
+ description: Origins in the cloudfront distribution
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of origins
+ returned: always
+ type: complex
+ contains:
+ custom_headers:
+ description: Custom headers passed to the origin
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of headers
+ returned: always
+ type: int
+ sample: 1
+ custom_origin_config:
+ description: Configuration of the origin
+ returned: always
+ type: complex
+ contains:
+ http_port:
+ description: Port on which HTTP is listening
+ returned: always
+ type: int
+ sample: 80
+ https_port:
+ description: Port on which HTTPS is listening
+ returned: always
+ type: int
+ sample: 443
+ origin_keepalive_timeout:
+ description: Keep-alive timeout
+ returned: always
+ type: int
+ sample: 5
+ origin_protocol_policy:
+ description: Policy of which protocols are supported
+ returned: always
+ type: string
+ sample: https-only
+ origin_read_timeout:
+ description: Timeout for reads to the origin
+ returned: always
+ type: int
+ sample: 30
+ origin_ssl_protocols:
+ description: SSL protocols allowed by the origin
+ returned: always
+ type: complex
+ contains:
+ items:
+ description: List of SSL protocols
+ returned: always
+ type: list
+ sample:
+ - TLSv1
+ - TLSv1.1
+ - TLSv1.2
+ quantity:
+ description: Count of SSL protocols
+ returned: always
+ type: int
+ sample: 3
+ domain_name:
+ description: Domain name of the origin
+ returned: always
+ type: string
+ sample: test-origin.example.com
+ id:
+ description: ID of the origin
+ returned: always
+ type: string
+ sample: test-origin.example.com
+ origin_path:
+ description: Subdirectory to prefix the request from the S3 or HTTP origin
+ returned: always
+ type: string
+ sample: ''
+ quantity:
+ description: Count of origins
+ returned: always
+ type: int
+ sample: 1
+price_class:
+ description: Price class of cloudfront distribution
+ returned: always
+ type: string
+ sample: PriceClass_All
+restrictions:
+ description: Restrictions in use by Cloudfront
+ returned: always
+ type: complex
+ contains:
+ geo_restriction:
+ description: Controls the countries in which your content is distributed.
+ returned: always
+ type: complex
+ contains:
+ quantity:
+ description: Count of restrictions
+ returned: always
+ type: int
+ sample: 1
+ items:
+ description: List of country codes allowed or disallowed
+ returned: always
+ type: list
+ sample: xy
+ restriction_type:
+ description: Type of restriction
+ returned: always
+ type: string
+ sample: blacklist
+status:
+ description: Status of the cloudfront distribution
+ returned: always
+ type: string
+ sample: InProgress
+tags:
+ description: Distribution tags
+ returned: always
+ type: dict
+ sample:
+ Hello: World
+viewer_certificate:
+ description: Certificate used by cloudfront distribution
+ returned: always
+ type: complex
+ contains:
+ acm_certificate_arn:
+ description: ARN of ACM certificate
+ returned: when certificate comes from ACM
+ type: string
+ sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef
+ certificate:
+ description: Reference to certificate
+ returned: always
+ type: string
+ sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef
+ certificate_source:
+ description: Where certificate comes from
+ returned: always
+ type: string
+ sample: acm
+ minimum_protocol_version:
+ description: Minimum SSL/TLS protocol supported by this distribution
+ returned: always
+ type: string
+ sample: TLSv1
+ ssl_support_method:
+ description: Support for pre-SNI browsers or not
+ returned: always
+ type: string
+ sample: sni-only
+web_acl_id:
+ description: ID of Web Access Control List (from WAF service)
+ returned: always
+ type: string
+ sample: abcd1234-1234-abcd-abcd-abcd12345678
+'''
+
+from ansible.module_utils._text import to_text, to_native
+from ansible.module_utils.aws.core import AnsibleAWSModule
+from ansible.module_utils.aws.cloudfront_facts import CloudFrontFactsServiceManager
+from ansible.module_utils.ec2 import get_aws_connection_info
+from ansible.module_utils.ec2 import ec2_argument_spec, boto3_conn, compare_aws_tags
+from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list
+from ansible.module_utils.ec2 import snake_dict_to_camel_dict, boto3_tag_list_to_ansible_dict
+import datetime
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ try:
+ from ordereddict import OrderedDict
+ except ImportError:
+ pass # caught by AnsibleAWSModule (as python 2.6 + boto3 => ordereddict is installed)
+
+try:
+ import botocore
+except ImportError:
+ pass
+
+
+def change_dict_key_name(dictionary, old_key, new_key):
+ if old_key in dictionary:
+ dictionary[new_key] = dictionary.get(old_key)
+ dictionary.pop(old_key, None)
+ return dictionary
+
+
+def merge_validation_into_config(config, validated_node, node_name):
+ if validated_node is not None:
+ if isinstance(validated_node, dict):
+ config_node = config.get(node_name)
+ if config_node is not None:
+ config_node_items = list(config_node.items())
+ else:
+ config_node_items = []
+ config[node_name] = dict(config_node_items + list(validated_node.items()))
+ if isinstance(validated_node, list):
+ config[node_name] = list(set(config.get(node_name) + validated_node))
+ return config
+
+
+def ansible_list_to_cloudfront_list(list_items=None, include_quantity=True):
+ if list_items is None:
+ list_items = []
+ if not isinstance(list_items, list):
+ raise ValueError('Expected a list, got a {0} with value {1}'.format(type(list_items).__name__, str(list_items)))
+ result = {}
+ if include_quantity:
+ result['quantity'] = len(list_items)
+ if len(list_items) > 0:
+ result['items'] = list_items
+ return result
+
+
+def recursive_diff(dict1, dict2):
+ left = dict((k, v) for (k, v) in dict1.items() if k not in dict2)
+ right = dict((k, v) for (k, v) in dict2.items() if k not in dict1)
+ for k in (set(dict1.keys()) & set(dict2.keys())):
+ if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ result = recursive_diff(dict1[k], dict2[k])
+ if result:
+ left[k] = result[0]
+ right[k] = result[1]
+ elif dict1[k] != dict2[k]:
+ left[k] = dict1[k]
+ right[k] = dict2[k]
+ if left or right:
+ return left, right
+ else:
+ return None
+
+
+def create_distribution(client, module, config, tags):
+ try:
+ if not tags:
+ return client.create_distribution(DistributionConfig=config)['Distribution']
+ else:
+ distribution_config_with_tags = {
+ 'DistributionConfig': config,
+ 'Tags': {
+ 'Items': tags
+ }
+ }
+ return client.create_distribution_with_tags(DistributionConfigWithTags=distribution_config_with_tags)['Distribution']
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json_aws(e, msg="Error creating distribution")
+
+
+def delete_distribution(client, module, distribution):
+ try:
+ return client.delete_distribution(Id=distribution['Distribution']['Id'], IfMatch=distribution['ETag'])
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json_aws(e, msg="Error deleting distribution %s" % to_native(distribution['Distribution']))
+
+
+def update_distribution(client, module, config, distribution_id, e_tag):
+ try:
+ return client.update_distribution(DistributionConfig=config, Id=distribution_id, IfMatch=e_tag)['Distribution']
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json_aws(e, msg="Error updating distribution to %s" % to_native(config))
+
+
+def tag_resource(client, module, arn, tags):
+ try:
+ return client.tag_resource(Resource=arn, Tags=dict(Items=tags))
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json_aws(e, msg="Error tagging resource")
+
+
+def untag_resource(client, module, arn, tag_keys):
+ try:
+ return client.untag_resource(Resource=arn, TagKeys=dict(Items=tag_keys))
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json_aws(e, msg="Error untagging resource")
+
+
+def list_tags_for_resource(client, module, arn):
+ try:
+ response = client.list_tags_for_resource(Resource=arn)
+ return boto3_tag_list_to_ansible_dict(response.get('Tags').get('Items'))
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ module.fail_json_aws(e, msg="Error listing tags for resource")
+
+
+def update_tags(client, module, existing_tags, valid_tags, purge_tags, arn):
+ changed = False
+ to_add, to_remove = compare_aws_tags(existing_tags, valid_tags, purge_tags)
+ if to_remove:
+ untag_resource(client, module, arn, to_remove)
+ changed = True
+ if to_add:
+ tag_resource(client, module, arn, ansible_dict_to_boto3_tag_list(to_add))
+ changed = True
+ return changed
+
+
+class CloudFrontValidationManager(object):
+ """
+ Manages Cloudfront validations
+ """
+
+ def __init__(self, module):
+ self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module)
+ self.module = module
+ self.__default_distribution_enabled = True
+ self.__default_http_port = 80
+ self.__default_https_port = 443
+ self.__default_ipv6_enabled = False
+ self.__default_origin_ssl_protocols = [
+ 'TLSv1',
+ 'TLSv1.1',
+ 'TLSv1.2'
+ ]
+ self.__default_custom_origin_protocol_policy = 'match-viewer'
+ self.__default_custom_origin_read_timeout = 30
+ self.__default_custom_origin_keepalive_timeout = 5
+ self.__default_datetime_string = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')
+ self.__default_cache_behavior_min_ttl = 0
+ self.__default_cache_behavior_max_ttl = 31536000
+ self.__default_cache_behavior_default_ttl = 86400
+ self.__default_cache_behavior_compress = False
+ self.__default_cache_behavior_viewer_protocol_policy = 'allow-all'
+ self.__default_cache_behavior_smooth_streaming = False
+ self.__default_cache_behavior_forwarded_values_forward_cookies = 'none'
+ self.__default_cache_behavior_forwarded_values_query_string = True
+ self.__default_trusted_signers_enabled = False
+ self.__valid_price_classes = set([
+ 'PriceClass_100',
+ 'PriceClass_200',
+ 'PriceClass_All'
+ ])
+ self.__valid_origin_protocol_policies = set([
+ 'http-only',
+ 'match-viewer',
+ 'https-only'
+ ])
+ self.__valid_origin_ssl_protocols = set([
+ 'SSLv3',
+ 'TLSv1',
+ 'TLSv1.1',
+ 'TLSv1.2'
+ ])
+ self.__valid_cookie_forwarding = set([
+ 'none',
+ 'whitelist',
+ 'all'
+ ])
+ self.__valid_viewer_protocol_policies = set([
+ 'allow-all',
+ 'https-only',
+ 'redirect-to-https'
+ ])
+ self.__valid_methods = set([
+ 'GET',
+ 'HEAD',
+ 'POST',
+ 'PUT',
+ 'PATCH',
+ 'OPTIONS',
+ 'DELETE'
+ ])
+ self.__valid_methods_cached_methods = [
+ set([
+ 'GET',
+ 'HEAD'
+ ]),
+ set([
+ 'GET',
+ 'HEAD',
+ 'OPTIONS'
+ ])
+ ]
+ self.__valid_methods_allowed_methods = [
+ self.__valid_methods_cached_methods[0],
+ self.__valid_methods_cached_methods[1],
+ self.__valid_methods
+ ]
+ self.__valid_lambda_function_association_event_types = set([
+ 'viewer-request',
+ 'viewer-response',
+ 'origin-request',
+ 'origin-response'
+ ])
+ self.__valid_viewer_certificate_ssl_support_methods = set([
+ 'sni-only',
+ 'vip'
+ ])
+ self.__valid_viewer_certificate_minimum_protocol_versions = set([
+ 'SSLv3',
+ 'TLSv1'
+ ])
+ self.__valid_viewer_certificate_certificate_sources = set([
+ 'cloudfront',
+ 'iam',
+ 'acm'
+ ])
+ self.__valid_http_versions = set([
+ 'http1.1',
+ 'http2'
+ ])
+ self.__s3_bucket_domain_identifier = '.s3.amazonaws.com'
+
+ def add_missing_key(self, dict_object, key_to_set, value_to_set):
+ if key_to_set not in dict_object and value_to_set is not None:
+ dict_object[key_to_set] = value_to_set
+ return dict_object
+
+ def add_key_else_change_dict_key(self, dict_object, old_key, new_key, value_to_set):
+ if old_key not in dict_object and value_to_set is not None:
+ dict_object[new_key] = value_to_set
+ else:
+ dict_object = change_dict_key_name(dict_object, old_key, new_key)
+ return dict_object
+
+ def add_key_else_validate(self, dict_object, key_name, attribute_name, value_to_set, valid_values, to_aws_list=False):
+ if key_name in dict_object:
+ self.validate_attribute_with_allowed_values(value_to_set, attribute_name, valid_values)
+ else:
+ if to_aws_list:
+ dict_object[key_name] = ansible_list_to_cloudfront_list(value_to_set)
+ elif value_to_set is not None:
+ dict_object[key_name] = value_to_set
+ return dict_object
+
+ def validate_logging(self, logging):
+ try:
+ if logging is None:
+ return None
+ valid_logging = {}
+ if logging and not set(['enabled', 'include_cookies', 'bucket', 'prefix']).issubset(logging):
+ self.module.fail_json(msg="The logging parameters enabled, include_cookies, bucket and prefix must be specified.")
+ valid_logging['include_cookies'] = logging.get('include_cookies')
+ valid_logging['enabled'] = logging.get('enabled')
+ valid_logging['bucket'] = logging.get('bucket')
+ valid_logging['prefix'] = logging.get('prefix')
+ return valid_logging
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution logging")
+
+ def validate_is_list(self, list_to_validate, list_name):
+ if not isinstance(list_to_validate, list):
+ self.module.fail_json(msg='%s is of type %s. Must be a list.' % (list_name, type(list_to_validate).__name__))
+
+ def validate_required_key(self, key_name, full_key_name, dict_object):
+ if key_name not in dict_object:
+ self.module.fail_json(msg="%s must be specified." % full_key_name)
+
+ def validate_origins(self, client, config, origins, default_origin_domain_name,
+ default_origin_path, create_distribution, purge_origins=False):
+ try:
+ if origins is None:
+ if default_origin_domain_name is None and not create_distribution:
+ if purge_origins:
+ return None
+ else:
+ return ansible_list_to_cloudfront_list(config)
+ if default_origin_domain_name is not None:
+ origins = [{
+ 'domain_name': default_origin_domain_name,
+ 'origin_path': default_origin_path or ''
+ }]
+ else:
+ origins = []
+ self.validate_is_list(origins, 'origins')
+ if not origins and default_origin_domain_name is None and create_distribution:
+ self.module.fail_json(msg="Both origins[] and default_origin_domain_name have not been specified. Please specify at least one.")
+ all_origins = OrderedDict()
+ new_domains = list()
+ for origin in config:
+ all_origins[origin.get('domain_name')] = origin
+ for origin in origins:
+ origin = self.validate_origin(client, all_origins.get(origin.get('domain_name'), {}), origin, default_origin_path)
+ all_origins[origin['domain_name']] = origin
+ new_domains.append(origin['domain_name'])
+ if purge_origins:
+ for domain in all_origins:
+ if domain not in new_domains:
+ del(all_origins[domain])
+ return ansible_list_to_cloudfront_list(all_origins.values())
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution origins")
+
+ def validate_s3_origin_configuration(self, client, existing_config, origin):
+ if origin['s3_origin_access_identity_enabled'] and existing_config.get('s3_origin_config', {}).get('origin_access_identity'):
+ return existing_config['s3_origin_config']['origin_access_identity']
+ if not origin['s3_origin_access_identity_enabled']:
+ return None
+ try:
+ comment = "Origin Access Identity created by Ansible at %s" % self.__default_datetime_string
+ cfoai_config = dict(CloudFrontOriginAccessIdentityConfig=dict(CallerReference=self.__default_datetime_string,
+ Comment=comment))
+ oai = client.create_cloud_front_origin_access_identity(**cfoai_config)['CloudFrontOriginAccessIdentity']['Id']
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Couldn't create Origin Access Identity for id %s" % origin['id'])
+ return "origin-access-identity/cloudfront/%s" % oai
+
+ def validate_origin(self, client, existing_config, origin, default_origin_path):
+ try:
+ origin = self.add_missing_key(origin, 'origin_path', existing_config.get('origin_path', default_origin_path or ''))
+ self.validate_required_key('origin_path', 'origins[].origin_path', origin)
+ origin = self.add_missing_key(origin, 'id', existing_config.get('id', self.__default_datetime_string))
+ if 'custom_headers' in origin and len(origin.get('custom_headers')) > 0:
+ for custom_header in origin.get('custom_headers'):
+ if 'header_name' not in custom_header or 'header_value' not in custom_header:
+ self.module.fail_json(msg="Both origins[].custom_headers.header_name and origins[].custom_headers.header_value must be specified.")
+ origin['custom_headers'] = ansible_list_to_cloudfront_list(origin.get('custom_headers'))
+ else:
+ origin['custom_headers'] = ansible_list_to_cloudfront_list()
+ if self.__s3_bucket_domain_identifier in origin.get('domain_name').lower():
+ if origin.get("s3_origin_access_identity_enabled") is not None:
+ s3_origin_config = self.validate_s3_origin_configuration(client, existing_config, origin)
+ if s3_origin_config:
+ oai = s3_origin_config
+ else:
+ oai = ""
+ origin["s3_origin_config"] = dict(origin_access_identity=oai)
+ del(origin["s3_origin_access_identity_enabled"])
+ if 'custom_origin_config' in origin:
+ self.module.fail_json(msg="s3_origin_access_identity_enabled and custom_origin_config are mutually exclusive")
+ else:
+ origin = self.add_missing_key(origin, 'custom_origin_config', existing_config.get('custom_origin_config', {}))
+ custom_origin_config = origin.get('custom_origin_config')
+ custom_origin_config = self.add_key_else_validate(custom_origin_config, 'origin_protocol_policy',
+ 'origins[].custom_origin_config.origin_protocol_policy',
+ self.__default_custom_origin_protocol_policy, self.__valid_origin_protocol_policies)
+ custom_origin_config = self.add_missing_key(custom_origin_config, 'origin_read_timeout', self.__default_custom_origin_read_timeout)
+ custom_origin_config = self.add_missing_key(custom_origin_config, 'origin_keepalive_timeout', self.__default_custom_origin_keepalive_timeout)
+ custom_origin_config = self.add_key_else_change_dict_key(custom_origin_config, 'http_port', 'h_t_t_p_port', self.__default_http_port)
+ custom_origin_config = self.add_key_else_change_dict_key(custom_origin_config, 'https_port', 'h_t_t_p_s_port', self.__default_https_port)
+ if custom_origin_config.get('origin_ssl_protocols', {}).get('items'):
+ custom_origin_config['origin_ssl_protocols'] = custom_origin_config['origin_ssl_protocols']['items']
+ if custom_origin_config.get('origin_ssl_protocols'):
+ self.validate_attribute_list_with_allowed_list(custom_origin_config['origin_ssl_protocols'], 'origins[].origin_ssl_protocols',
+ self.__valid_origin_ssl_protocols)
+ else:
+ custom_origin_config['origin_ssl_protocols'] = self.__default_origin_ssl_protocols
+ custom_origin_config['origin_ssl_protocols'] = ansible_list_to_cloudfront_list(custom_origin_config['origin_ssl_protocols'])
+ return origin
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution origin")
+
+ def validate_cache_behaviors(self, config, cache_behaviors, valid_origins, purge_cache_behaviors=False):
+ try:
+ if cache_behaviors is None and valid_origins is not None and purge_cache_behaviors is False:
+ return ansible_list_to_cloudfront_list(config)
+ all_cache_behaviors = OrderedDict()
+ # cache behaviors are order dependent so we don't preserve the existing ordering when purge_cache_behaviors
+ # is true (if purge_cache_behaviors is not true, we can't really know the full new order)
+ if not purge_cache_behaviors:
+ for behavior in config:
+ all_cache_behaviors[behavior['path_pattern']] = behavior
+ for cache_behavior in cache_behaviors:
+ valid_cache_behavior = self.validate_cache_behavior(all_cache_behaviors.get(cache_behavior.get('path_pattern'), {}),
+ cache_behavior, valid_origins)
+ all_cache_behaviors[cache_behavior['path_pattern']] = valid_cache_behavior
+ if purge_cache_behaviors:
+ for target_origin_id in set(all_cache_behaviors.keys()) - set([cb['path_pattern'] for cb in cache_behaviors]):
+ del(all_cache_behaviors[target_origin_id])
+ return ansible_list_to_cloudfront_list(all_cache_behaviors.values())
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution cache behaviors")
+
+ def validate_cache_behavior(self, config, cache_behavior, valid_origins, is_default_cache=False):
+ if is_default_cache and cache_behavior is None:
+ cache_behavior = {}
+ if cache_behavior is None and valid_origins is not None:
+ return config
+ cache_behavior = self.validate_cache_behavior_first_level_keys(config, cache_behavior, valid_origins, is_default_cache)
+ cache_behavior = self.validate_forwarded_values(config, cache_behavior.get('forwarded_values'), cache_behavior)
+ cache_behavior = self.validate_allowed_methods(config, cache_behavior.get('allowed_methods'), cache_behavior)
+ cache_behavior = self.validate_lambda_function_associations(config, cache_behavior.get('lambda_function_associations'), cache_behavior)
+ cache_behavior = self.validate_trusted_signers(config, cache_behavior.get('trusted_signers'), cache_behavior)
+ return cache_behavior
+
+ def validate_cache_behavior_first_level_keys(self, config, cache_behavior, valid_origins, is_default_cache):
+ try:
+ cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'min_ttl', 'min_t_t_l',
+ config.get('min_t_t_l', self.__default_cache_behavior_min_ttl))
+ cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'max_ttl', 'max_t_t_l',
+ config.get('max_t_t_l', self.__default_cache_behavior_max_ttl))
+ cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'default_ttl', 'default_t_t_l',
+ config.get('default_t_t_l', self.__default_cache_behavior_default_ttl))
+ cache_behavior = self.add_missing_key(cache_behavior, 'compress', config.get('compress', self.__default_cache_behavior_compress))
+ target_origin_id = cache_behavior.get('target_origin_id', config.get('target_origin_id'))
+ if not target_origin_id:
+ target_origin_id = self.get_first_origin_id_for_default_cache_behavior(valid_origins)
+ if target_origin_id not in [origin['id'] for origin in valid_origins.get('items', [])]:
+ if is_default_cache:
+ cache_behavior_name = 'Default cache behavior'
+ else:
+ cache_behavior_name = 'Cache behavior for path %s' % cache_behavior['path_pattern']
+ self.module.fail_json(msg="%s has target_origin_id pointing to an origin that does not exist." %
+ cache_behavior_name)
+ cache_behavior['target_origin_id'] = target_origin_id
+ cache_behavior = self.add_key_else_validate(cache_behavior, 'viewer_protocol_policy', 'cache_behavior.viewer_protocol_policy',
+ config.get('viewer_protocol_policy',
+ self.__default_cache_behavior_viewer_protocol_policy),
+ self.__valid_viewer_protocol_policies)
+ cache_behavior = self.add_missing_key(cache_behavior, 'smooth_streaming',
+ config.get('smooth_streaming', self.__default_cache_behavior_smooth_streaming))
+ return cache_behavior
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution cache behavior first level keys")
+
+ def validate_forwarded_values(self, config, forwarded_values, cache_behavior):
+ try:
+ if not forwarded_values:
+ forwarded_values = dict()
+ existing_config = config.get('forwarded_values', {})
+ headers = forwarded_values.get('headers', existing_config.get('headers', {}).get('items'))
+ forwarded_values['headers'] = ansible_list_to_cloudfront_list(headers)
+ if 'cookies' not in forwarded_values:
+ forward = existing_config.get('cookies', {}).get('forward', self.__default_cache_behavior_forwarded_values_forward_cookies)
+ forwarded_values['cookies'] = {'forward': forward}
+ else:
+ existing_whitelist = existing_config.get('cookies', {}).get('whitelisted_names', {}).get('items')
+ whitelist = forwarded_values.get('cookies').get('whitelisted_names', existing_whitelist)
+ if whitelist:
+ self.validate_is_list(whitelist, 'forwarded_values.whitelisted_names')
+ forwarded_values['cookies']['whitelisted_names'] = ansible_list_to_cloudfront_list(whitelist)
+ cookie_forwarding = forwarded_values.get('cookies').get('forward', existing_config.get('cookies', {}).get('forward'))
+ self.validate_attribute_with_allowed_values(cookie_forwarding, 'cache_behavior.forwarded_values.cookies.forward',
+ self.__valid_cookie_forwarding)
+ forwarded_values['cookies']['forward'] = cookie_forwarding
+ query_string_cache_keys = forwarded_values.get('query_string_cache_keys', existing_config.get('query_string_cache_keys', {}).get('items', []))
+ self.validate_is_list(query_string_cache_keys, 'forwarded_values.query_string_cache_keys')
+ forwarded_values['query_string_cache_keys'] = ansible_list_to_cloudfront_list(query_string_cache_keys)
+ forwarded_values = self.add_missing_key(forwarded_values, 'query_string',
+ existing_config.get('query_string', self.__default_cache_behavior_forwarded_values_query_string))
+ cache_behavior['forwarded_values'] = forwarded_values
+ return cache_behavior
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating forwarded values")
+
+ def validate_lambda_function_associations(self, config, lambda_function_associations, cache_behavior):
+ try:
+ if lambda_function_associations is not None:
+ self.validate_is_list(lambda_function_associations, 'lambda_function_associations')
+ for association in lambda_function_associations:
+ association = change_dict_key_name(association, 'lambda_function_arn', 'lambda_function_a_r_n')
+ self.validate_attribute_with_allowed_values(association.get('event_type'), 'cache_behaviors[].lambda_function_associations.event_type',
+ self.__valid_lambda_function_association_event_types)
+ cache_behavior['lambda_function_associations'] = ansible_list_to_cloudfront_list(lambda_function_associations)
+ else:
+ if 'lambda_function_associations' in config:
+ cache_behavior['lambda_function_associations'] = config.get('lambda_function_associations')
+ else:
+ cache_behavior['lambda_function_associations'] = ansible_list_to_cloudfront_list([])
+ return cache_behavior
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating lambda function associations")
+
+ def validate_allowed_methods(self, config, allowed_methods, cache_behavior):
+ try:
+ if allowed_methods is not None:
+ self.validate_required_key('items', 'cache_behavior.allowed_methods.items[]', allowed_methods)
+ temp_allowed_items = allowed_methods.get('items')
+ self.validate_is_list(temp_allowed_items, 'cache_behavior.allowed_methods.items')
+ self.validate_attribute_list_with_allowed_list(temp_allowed_items, 'cache_behavior.allowed_methods.items[]',
+ self.__valid_methods_allowed_methods)
+ cached_items = allowed_methods.get('cached_methods')
+ if 'cached_methods' in allowed_methods:
+ self.validate_is_list(cached_items, 'cache_behavior.allowed_methods.cached_methods')
+ self.validate_attribute_list_with_allowed_list(cached_items, 'cache_behavior.allowed_items.cached_methods[]',
+ self.__valid_methods_cached_methods)
+ # we don't care if the order of how cloudfront stores the methods differs - preserving existing
+ # order reduces likelihood of making unnecessary changes
+ if 'allowed_methods' in config and set(config['allowed_methods']['items']) == set(temp_allowed_items):
+ cache_behavior['allowed_methods'] = config['allowed_methods']
+ else:
+ cache_behavior['allowed_methods'] = ansible_list_to_cloudfront_list(temp_allowed_items)
+
+ if cached_items and set(cached_items) == set(config.get('allowed_methods', {}).get('cached_methods', {}).get('items', [])):
+ cache_behavior['allowed_methods']['cached_methods'] = config['allowed_methods']['cached_methods']
+ else:
+ cache_behavior['allowed_methods']['cached_methods'] = ansible_list_to_cloudfront_list(cached_items)
+ else:
+ if 'allowed_methods' in config:
+ cache_behavior['allowed_methods'] = config.get('allowed_methods')
+ return cache_behavior
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating allowed methods")
+
+ def validate_trusted_signers(self, config, trusted_signers, cache_behavior):
+ try:
+ if trusted_signers is None:
+ trusted_signers = {}
+ if 'items' in trusted_signers:
+ valid_trusted_signers = ansible_list_to_cloudfront_list(trusted_signers.get('items'))
+ else:
+ valid_trusted_signers = dict(quantity=config.get('quantity', 0))
+ if 'items' in config:
+ valid_trusted_signers = dict(items=config['items'])
+ valid_trusted_signers['enabled'] = trusted_signers.get('enabled', config.get('enabled', self.__default_trusted_signers_enabled))
+ cache_behavior['trusted_signers'] = valid_trusted_signers
+ return cache_behavior
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating trusted signers")
+
+ def validate_viewer_certificate(self, viewer_certificate):
+ try:
+ if viewer_certificate is None:
+ return None
+ if viewer_certificate.get('cloudfront_default_certificate') and viewer_certificate.get('ssl_support_method') is not None:
+ self.module.fail_json(msg="viewer_certificate.ssl_support_method should not be specified with viewer_certificate_cloudfront_default" +
+ "_certificate set to true.")
+ self.validate_attribute_with_allowed_values(viewer_certificate.get('ssl_support_method'), 'viewer_certificate.ssl_support_method',
+ self.__valid_viewer_certificate_ssl_support_methods)
+ self.validate_attribute_with_allowed_values(viewer_certificate.get('minimum_protocol_version'), 'viewer_certificate.minimum_protocol_version',
+ self.__valid_viewer_certificate_minimum_protocol_versions)
+ self.validate_attribute_with_allowed_values(viewer_certificate.get('certificate_source'), 'viewer_certificate.certificate_source',
+ self.__valid_viewer_certificate_certificate_sources)
+ viewer_certificate = change_dict_key_name(viewer_certificate, 'cloudfront_default_certificate', 'cloud_front_default_certificate')
+ viewer_certificate = change_dict_key_name(viewer_certificate, 'ssl_support_method', 's_s_l_support_method')
+ viewer_certificate = change_dict_key_name(viewer_certificate, 'iam_certificate_id', 'i_a_m_certificate_id')
+ viewer_certificate = change_dict_key_name(viewer_certificate, 'acm_certificate_arn', 'a_c_m_certificate_arn')
+ return viewer_certificate
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating viewer certificate")
+
+ def validate_custom_error_responses(self, config, custom_error_responses, purge_custom_error_responses):
+ try:
+ if custom_error_responses is None and not purge_custom_error_responses:
+ return ansible_list_to_cloudfront_list(config)
+ self.validate_is_list(custom_error_responses, 'custom_error_responses')
+ result = list()
+ existing_responses = dict((response['error_code'], response) for response in custom_error_responses)
+ for custom_error_response in custom_error_responses:
+ self.validate_required_key('error_code', 'custom_error_responses[].error_code', custom_error_response)
+ custom_error_response = change_dict_key_name(custom_error_response, 'error_caching_min_ttl', 'error_caching_min_t_t_l')
+ if 'response_code' in custom_error_response:
+ custom_error_response['response_code'] = str(custom_error_response['response_code'])
+ if custom_error_response['error_code'] in existing_responses:
+ del(existing_responses[custom_error_response['error_code']])
+ result.append(custom_error_response)
+ if not purge_custom_error_responses:
+ result.extend(existing_responses.values())
+
+ return ansible_list_to_cloudfront_list(result)
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating custom error responses")
+
+ def validate_restrictions(self, config, restrictions, purge_restrictions=False):
+ try:
+ if restrictions is None:
+ if purge_restrictions:
+ return None
+ else:
+ return config
+ self.validate_required_key('geo_restriction', 'restrictions.geo_restriction', restrictions)
+ geo_restriction = restrictions.get('geo_restriction')
+ self.validate_required_key('restriction_type', 'restrictions.geo_restriction.restriction_type', geo_restriction)
+ existing_restrictions = config.get('geo_restriction', {}).get(geo_restriction['restriction_type'], {}).get('items', [])
+ geo_restriction_items = geo_restriction.get('items')
+ if not purge_restrictions:
+ geo_restriction_items.extend([rest for rest in existing_restrictions if
+ rest not in geo_restriction_items])
+ valid_restrictions = ansible_list_to_cloudfront_list(geo_restriction_items)
+ valid_restrictions['restriction_type'] = geo_restriction.get('restriction_type')
+ return valid_restrictions
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating restrictions")
+
+ def validate_distribution_config_parameters(self, config, default_root_object, ipv6_enabled, http_version, web_acl_id):
+ try:
+ config['default_root_object'] = default_root_object or config.get('default_root_object', '')
+ config['is_i_p_v_6_enabled'] = ipv6_enabled or config.get('i_p_v_6_enabled', self.__default_ipv6_enabled)
+ if http_version is not None or config.get('http_version'):
+ self.validate_attribute_with_allowed_values(http_version, 'http_version', self.__valid_http_versions)
+ config['http_version'] = http_version or config.get('http_version')
+ if web_acl_id or config.get('web_a_c_l_id'):
+ config['web_a_c_l_id'] = web_acl_id or config.get('web_a_c_l_id')
+ return config
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution config parameters")
+
+ def validate_common_distribution_parameters(self, config, enabled, aliases, logging, price_class, purge_aliases=False):
+ try:
+ if config is None:
+ config = {}
+ if aliases is not None:
+ if not purge_aliases:
+ aliases.extend([alias for alias in config.get('aliases', {}).get('items', [])
+ if alias not in aliases])
+ config['aliases'] = ansible_list_to_cloudfront_list(aliases)
+ if logging is not None:
+ config['logging'] = self.validate_logging(logging)
+ config['enabled'] = enabled or config.get('enabled', self.__default_distribution_enabled)
+ if price_class is not None:
+ self.validate_attribute_with_allowed_values(price_class, 'price_class', self.__valid_price_classes)
+ config['price_class'] = price_class
+ return config
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating common distribution parameters")
+
+ def validate_comment(self, config, comment):
+ config['comment'] = comment or config.get('comment', "Distribution created by Ansible with datetime stamp " + self.__default_datetime_string)
+ return config
+
+ def validate_caller_reference(self, caller_reference):
+ return caller_reference or self.__default_datetime_string
+
+ def get_first_origin_id_for_default_cache_behavior(self, valid_origins):
+ try:
+ if valid_origins is not None:
+ valid_origins_list = valid_origins.get('items')
+ if valid_origins_list is not None and isinstance(valid_origins_list, list) and len(valid_origins_list) > 0:
+ return str(valid_origins_list[0].get('id'))
+ self.module.fail_json(msg="There are no valid origins from which to specify a target_origin_id for the default_cache_behavior configuration.")
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error getting first origin_id for default cache behavior")
+
+ def validate_attribute_list_with_allowed_list(self, attribute_list, attribute_list_name, allowed_list):
+ try:
+ self.validate_is_list(attribute_list, attribute_list_name)
+ if (isinstance(allowed_list, list) and set(attribute_list) not in allowed_list or
+ isinstance(allowed_list, set) and not set(allowed_list).issuperset(attribute_list)):
+ self.module.fail_json(msg='The attribute list {0} must be one of [{1}]'.format(attribute_list_name, ' '.join(str(a) for a in allowed_list)))
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating attribute list with allowed value list")
+
+ def validate_attribute_with_allowed_values(self, attribute, attribute_name, allowed_list):
+ if attribute is not None and attribute not in allowed_list:
+ self.module.fail_json(msg='The attribute {0} must be one of [{1}]'.format(attribute_name, ' '.join(str(a) for a in allowed_list)))
+
+ def validate_distribution_from_caller_reference(self, caller_reference):
+ try:
+ distributions = self.__cloudfront_facts_mgr.list_distributions(False)
+ distribution_name = 'Distribution'
+ distribution_config_name = 'DistributionConfig'
+ distribution_ids = [dist.get('Id') for dist in distributions]
+ for distribution_id in distribution_ids:
+ config = self.__cloudfront_facts_mgr.get_distribution(distribution_id)
+ distribution = config.get(distribution_name)
+ if distribution is not None:
+ distribution_config = distribution.get(distribution_config_name)
+ if distribution_config is not None and distribution_config.get('CallerReference') == caller_reference:
+ distribution['DistributionConfig'] = distribution_config
+ return distribution
+
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution from caller reference")
+
+ def validate_distribution_from_aliases_caller_reference(self, distribution_id, aliases, caller_reference):
+ try:
+ if caller_reference is not None:
+ return self.validate_distribution_from_caller_reference(caller_reference)
+ else:
+ if aliases:
+ distribution_id = self.validate_distribution_id_from_alias(aliases)
+ if distribution_id:
+ return self.__cloudfront_facts_mgr.get_distribution(distribution_id)
+ return None
+ except Exception as e:
+ self.module.fail_json_aws(e, msg="Error validating distribution_id from alias, aliases and caller reference")
+
+ def validate_distribution_id_from_alias(self, aliases):
+ distributions = self.__cloudfront_facts_mgr.list_distributions(False)
+ if distributions:
+ for distribution in distributions:
+ distribution_aliases = distribution.get('Aliases', {}).get('Items', [])
+ if set(aliases) & set(distribution_aliases):
+ return distribution['Id']
+ return None
+
+ def wait_until_processed(self, client, wait_timeout, distribution_id, caller_reference):
+ if distribution_id is None:
+ distribution_id = self.validate_distribution_id_from_caller_reference(caller_reference=caller_reference)
+
+ try:
+ waiter = client.get_waiter('distribution_deployed')
+ attempts = 1 + int(wait_timeout / 60)
+ waiter.wait(Id=distribution_id, WaiterConfig={'MaxAttempts': attempts})
+ except botocore.exceptions.WaiterError as e:
+ self.module.fail_json(msg="Timeout waiting for cloudfront action. Waited for {0} seconds before timeout. "
+ "Error: {1}".format(to_text(wait_timeout), to_native(e)))
+
+ except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+ self.module.fail_json_aws(e, msg="Error getting distribution {0}".format(distribution_id))
+
+
+def main():
+ argument_spec = ec2_argument_spec()
+
+ argument_spec.update(dict(
+ state=dict(choices=['present', 'absent'], default='present'),
+ caller_reference=dict(),
+ comment=dict(),
+ distribution_id=dict(),
+ e_tag=dict(),
+ tags=dict(type='dict', default={}),
+ purge_tags=dict(type='bool', default=False),
+ alias=dict(),
+ aliases=dict(type='list', default=[]),
+ purge_aliases=dict(type='bool', default=False),
+ default_root_object=dict(),
+ origins=dict(type='list'),
+ purge_origins=dict(type='bool', default=False),
+ default_cache_behavior=dict(type='dict'),
+ cache_behaviors=dict(type='list'),
+ purge_cache_behaviors=dict(type='bool', default=False),
+ custom_error_responses=dict(type='list'),
+ purge_custom_error_responses=dict(type='bool', default=False),
+ logging=dict(type='dict'),
+ price_class=dict(),
+ enabled=dict(type='bool'),
+ viewer_certificate=dict(type='dict'),
+ restrictions=dict(type='dict'),
+ web_acl_id=dict(),
+ http_version=dict(),
+ ipv6_enabled=dict(type='bool'),
+ default_origin_domain_name=dict(),
+ default_origin_path=dict(),
+ wait=dict(default=False, type='bool'),
+ wait_timeout=dict(default=1800, type='int')
+ ))
+
+ result = {}
+ changed = True
+
+ module = AnsibleAWSModule(
+ argument_spec=argument_spec,
+ supports_check_mode=False,
+ mutually_exclusive=[
+ ['distribution_id', 'alias'],
+ ['default_origin_domain_name', 'distribution_id'],
+ ['default_origin_domain_name', 'alias'],
+ ]
+ )
+
+ region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
+ client = boto3_conn(module, conn_type='client', resource='cloudfront', region=region, endpoint=ec2_url, **aws_connect_kwargs)
+
+ validation_mgr = CloudFrontValidationManager(module)
+
+ state = module.params.get('state')
+ caller_reference = module.params.get('caller_reference')
+ comment = module.params.get('comment')
+ e_tag = module.params.get('e_tag')
+ tags = module.params.get('tags')
+ purge_tags = module.params.get('purge_tags')
+ distribution_id = module.params.get('distribution_id')
+ alias = module.params.get('alias')
+ aliases = module.params.get('aliases')
+ purge_aliases = module.params.get('purge_aliases')
+ default_root_object = module.params.get('default_root_object')
+ origins = module.params.get('origins')
+ purge_origins = module.params.get('purge_origins')
+ default_cache_behavior = module.params.get('default_cache_behavior')
+ cache_behaviors = module.params.get('cache_behaviors')
+ purge_cache_behaviors = module.params.get('purge_cache_behaviors')
+ custom_error_responses = module.params.get('custom_error_responses')
+ purge_custom_error_responses = module.params.get('purge_custom_error_responses')
+ logging = module.params.get('logging')
+ price_class = module.params.get('price_class')
+ enabled = module.params.get('enabled')
+ viewer_certificate = module.params.get('viewer_certificate')
+ restrictions = module.params.get('restrictions')
+ purge_restrictions = module.params.get('purge_restrictions')
+ web_acl_id = module.params.get('web_acl_id')
+ http_version = module.params.get('http_version')
+ ipv6_enabled = module.params.get('ipv6_enabled')
+ default_origin_domain_name = module.params.get('default_origin_domain_name')
+ default_origin_path = module.params.get('default_origin_path')
+ wait = module.params.get('wait')
+ wait_timeout = module.params.get('wait_timeout')
+
+ if alias and alias not in aliases:
+ aliases.append(alias)
+
+ distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(distribution_id, aliases, caller_reference)
+
+ update = state == 'present' and distribution
+ create = state == 'present' and not distribution
+ delete = state == 'absent' and distribution
+
+ if not (update or create or delete):
+ module.exit_json(changed=False)
+
+ if update or delete:
+ config = distribution['Distribution']['DistributionConfig']
+ e_tag = distribution['ETag']
+ distribution_id = distribution['Distribution']['Id']
+ else:
+ config = dict()
+ if update:
+ config = camel_dict_to_snake_dict(config, reversible=True)
+
+ if create or update:
+ config = validation_mgr.validate_common_distribution_parameters(config, enabled, aliases, logging, price_class, purge_aliases)
+ config = validation_mgr.validate_distribution_config_parameters(config, default_root_object, ipv6_enabled, http_version, web_acl_id)
+ config['origins'] = validation_mgr.validate_origins(client, config.get('origins', {}).get('items', []), origins, default_origin_domain_name,
+ default_origin_path, create, purge_origins)
+ config['cache_behaviors'] = validation_mgr.validate_cache_behaviors(config.get('cache_behaviors', {}).get('items', []),
+ cache_behaviors, config['origins'], purge_cache_behaviors)
+ config['default_cache_behavior'] = validation_mgr.validate_cache_behavior(config.get('default_cache_behavior', {}),
+ default_cache_behavior, config['origins'], True)
+ config['custom_error_responses'] = validation_mgr.validate_custom_error_responses(config.get('custom_error_responses', {}).get('items', []),
+ custom_error_responses, purge_custom_error_responses)
+ valid_restrictions = validation_mgr.validate_restrictions(config.get('restrictions', {}), restrictions, purge_restrictions)
+ if valid_restrictions:
+ config['restrictions'] = valid_restrictions
+ valid_viewer_certificate = validation_mgr.validate_viewer_certificate(viewer_certificate)
+ config = merge_validation_into_config(config, valid_viewer_certificate, 'viewer_certificate')
+ config = validation_mgr.validate_comment(config, comment)
+ config = snake_dict_to_camel_dict(config, capitalize_first=True)
+
+ if create:
+ config['CallerReference'] = validation_mgr.validate_caller_reference(caller_reference)
+ result = create_distribution(client, module, config, ansible_dict_to_boto3_tag_list(tags))
+ result = camel_dict_to_snake_dict(result)
+ result['tags'] = list_tags_for_resource(client, module, result['arn'])
+
+ if delete:
+ if config['Enabled']:
+ config['Enabled'] = False
+ result = update_distribution(client, module, config, distribution_id, e_tag)
+ validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get('CallerReference'))
+ distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(distribution_id, aliases, caller_reference)
+ # e_tag = distribution['ETag']
+ result = delete_distribution(client, module, distribution)
+
+ if update:
+ changed = config != distribution['Distribution']['DistributionConfig']
+ if changed:
+ result = update_distribution(client, module, config, distribution_id, e_tag)
+ else:
+ result = distribution['Distribution']
+ existing_tags = list_tags_for_resource(client, module, result['ARN'])
+ distribution['Distribution']['DistributionConfig']['tags'] = existing_tags
+ changed |= update_tags(client, module, existing_tags, tags, purge_tags, result['ARN'])
+ result = camel_dict_to_snake_dict(result)
+ result['distribution_config']['tags'] = config['tags'] = list_tags_for_resource(client, module, result['arn'])
+ result['diff'] = dict()
+ diff = recursive_diff(distribution['Distribution']['DistributionConfig'], config)
+ if diff:
+ result['diff']['before'] = diff[0]
+ result['diff']['after'] = diff[1]
+
+ if wait and (create or update):
+ validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get('CallerReference'))
+
+ if 'distribution_config' in result:
+ result.update(result['distribution_config'])
+ del(result['distribution_config'])
+
+ module.exit_json(changed=changed, **result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/lib/ansible/modules/cloud/amazon/cloudfront_facts.py b/lib/ansible/modules/cloud/amazon/cloudfront_facts.py
index da12f3530a..43756edb33 100644
--- a/lib/ansible/modules/cloud/amazon/cloudfront_facts.py
+++ b/lib/ansible/modules/cloud/amazon/cloudfront_facts.py
@@ -670,5 +670,6 @@ def main():
result['cloudfront'].update(facts)
module.exit_json(msg="Retrieved cloudfront facts.", ansible_facts=result)
+
if __name__ == '__main__':
main()
diff --git a/test/integration/targets/cloudfront_distribution/aliases b/test/integration/targets/cloudfront_distribution/aliases
new file mode 100644
index 0000000000..4ef4b2067d
--- /dev/null
+++ b/test/integration/targets/cloudfront_distribution/aliases
@@ -0,0 +1 @@
+cloud/aws
diff --git a/test/integration/targets/cloudfront_distribution/defaults/main.yml b/test/integration/targets/cloudfront_distribution/defaults/main.yml
new file mode 100644
index 0000000000..ce7d1ebc41
--- /dev/null
+++ b/test/integration/targets/cloudfront_distribution/defaults/main.yml
@@ -0,0 +1,37 @@
+cloudfront_hostname: "{{ resource_prefix | lower }}01"
+# Use a domain that has a wildcard DNS
+cloudfront_alias: "{{ cloudfront_hostname | lower }}.github.io"
+
+cloudfront_test_cache_behaviors:
+ - path_pattern: /test/path
+ forwarded_values:
+ headers:
+ - Host
+ allowed_methods:
+ items:
+ - GET
+ - HEAD
+ - POST
+ - PATCH
+ - PUT
+ - OPTIONS
+ - DELETE
+ cached_methods:
+ - GET
+ - HEAD
+ - path_pattern: /another/path
+ forwarded_values:
+ cookies:
+ forward: whitelist
+ whitelisted_names:
+ - my_header
+ query_string: yes
+ query_string_cache_keys:
+ - whatever
+ allowed_methods:
+ items:
+ - GET
+ - HEAD
+ cached_methods:
+ - GET
+ - HEAD
diff --git a/test/integration/targets/cloudfront_distribution/meta/main.yml b/test/integration/targets/cloudfront_distribution/meta/main.yml
new file mode 100644
index 0000000000..1f64f1169a
--- /dev/null
+++ b/test/integration/targets/cloudfront_distribution/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+ - prepare_tests
+ - setup_ec2
diff --git a/test/integration/targets/cloudfront_distribution/tasks/main.yml b/test/integration/targets/cloudfront_distribution/tasks/main.yml
new file mode 100644
index 0000000000..5a26adb723
--- /dev/null
+++ b/test/integration/targets/cloudfront_distribution/tasks/main.yml
@@ -0,0 +1,386 @@
+- block:
+ - name: make sure resource prefix is lowercase
+ set_fact:
+ test_identifier: "{{ resource_prefix | lower }}"
+
+ - name: set yaml anchor
+ set_fact:
+ aws_connection_info: &aws_connection_info
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ security_token: "{{ security_token }}"
+ no_log: yes
+
+ - name: create cloudfront distribution using defaults
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ cloudfront_hostname }}-origin.example.com"
+ id: "{{ cloudfront_hostname }}-origin.example.com"
+ default_cache_behavior:
+ target_origin_id: "{{ cloudfront_hostname }}-origin.example.com"
+ state: present
+ purge_origins: yes
+ <<: *aws_connection_info
+
+ - name: re-run cloudfront distribution with same defaults
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ cloudfront_hostname }}-origin.example.com"
+ state: present
+ <<: *aws_connection_info
+ register: cf_dist_no_update
+
+ - name: ensure distribution was not updated
+ assert:
+ that:
+ - not cf_dist_no_update.changed
+
+ - name: update origin http port
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ cloudfront_hostname }}-origin.example.com"
+ custom_origin_config:
+ http_port: 8080
+ state: present
+ <<: *aws_connection_info
+ register: update_origin_http_port
+
+ - name: ensure http port was updated
+ assert:
+ that:
+ - update_origin_http_port.changed
+
+ - name: set a random comment
+ set_fact:
+ comment: "{{'ABCDEFabcdef123456'|shuffle|join }}"
+
+ - name: update comment
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ comment: "{{ comment }}"
+ state: present
+ <<: *aws_connection_info
+ register: cf_comment
+
+ - name: ensure comment was updated
+ assert:
+ that:
+ - cf_comment.changed
+ - 'cf_comment.comment == comment'
+
+ - name: create second origin
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ id: "{{ test_identifier }}2.example.com"
+ state: present
+ wait: yes
+ <<: *aws_connection_info
+ register: cf_add_origin
+
+ - name: ensure origin was added
+ assert:
+ that:
+ - cf_add_origin.origins.quantity == 2
+ - cf_add_origin.changed
+
+ - name: re-run second origin
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ cloudfront_hostname }}-origin.example.com"
+ custom_origin_config:
+ http_port: 8080
+ - domain_name: "{{ test_identifier }}2.example.com"
+ wait: yes
+ state: present
+ <<: *aws_connection_info
+ register: cf_rerun_second_origin
+
+ - name: ensure nothing changed after re-run
+ assert:
+ that:
+ - cf_rerun_second_origin.origins.quantity == 2
+ - not cf_rerun_second_origin.changed
+
+ - name: run with origins in reverse order
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ - domain_name: "{{ cloudfront_hostname }}-origin.example.com"
+ custom_origin_config:
+ http_port: 8080
+ state: present
+ <<: *aws_connection_info
+ register: cf_rerun_second_origin_reversed
+
+ - name: ensure nothing changed after reversed re-run
+ assert:
+ that:
+ - cf_rerun_second_origin_reversed.origins.quantity == 2
+ - not cf_rerun_second_origin_reversed.changed
+
+
+ - name: purge first origin
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ default_cache_behavior:
+ target_origin_id: "{{ test_identifier }}2.example.com"
+ purge_origins: yes
+ state: present
+ <<: *aws_connection_info
+ register: cf_purge_origin
+
+ - name: ensure origin was removed
+ assert:
+ that:
+ - cf_purge_origin.origins.quantity == 1
+ - cf_purge_origin.changed
+
+ - name: add tags to existing distribution
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ tags:
+ Name: "{{ cloudfront_alias }}"
+ Another: tag
+ state: present
+ <<: *aws_connection_info
+ register: cf_add_tags
+
+ - name: ensure tags were added
+ assert:
+ that:
+ - cf_add_tags.changed
+ - cf_add_tags.tags|length == 2
+
+ - name: delete distribution
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ enabled: no
+ wait: yes
+ state: absent
+ <<: *aws_connection_info
+
+ - name: create distribution with tags
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ id: "{{ test_identifier }}2.example.com"
+ tags:
+ Name: "{{ cloudfront_alias }}"
+ Another: tag
+ state: present
+ <<: *aws_connection_info
+ register: cf_second_distribution
+
+ - name: ensure tags were set on creation
+ assert:
+ that:
+ - cf_second_distribution.changed
+ - cf_second_distribution.tags|length == 2
+ - "'Name' in cf_second_distribution.tags"
+
+ - name: re-run create distribution with same tags and purge_tags
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ id: "{{ test_identifier }}2.example.com"
+ tags:
+ Name: "{{ cloudfront_alias }}"
+ Another: tag
+ purge_tags: yes
+ state: present
+ <<: *aws_connection_info
+ register: rerun_with_purge_tags
+
+ - name: ensure that re-running didn't change
+ assert:
+ that:
+ - not rerun_with_purge_tags.changed
+
+ - name: add new tag to distribution
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ tags:
+ Third: thing
+ purge_tags: no
+ state: present
+ <<: *aws_connection_info
+ register: update_with_new_tag
+
+ - name: ensure tags are correct
+ assert:
+ that:
+ - update_with_new_tag.changed
+ - "'Third' in update_with_new_tag.tags"
+ - "'Another' in update_with_new_tag.tags"
+
+ - name: create some cache behaviors
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ cache_behaviors: "{{ cloudfront_test_cache_behaviors }}"
+ state: present
+ <<: *aws_connection_info
+ register: add_cache_behaviors
+
+ - name: reverse some cache behaviors
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ cache_behaviors: "{{ cloudfront_test_cache_behaviors|reverse|list }}"
+ state: present
+ <<: *aws_connection_info
+ register: reverse_cache_behaviors
+
+ - name: check that reversing cache behaviors changes nothing when purge_cache_behaviors unset
+ assert:
+ that:
+ - not reverse_cache_behaviors.changed
+ - reverse_cache_behaviors.cache_behaviors|length == 2
+
+ - name: reverse some cache behaviors properly
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}2.example.com"
+ cache_behaviors: "{{ cloudfront_test_cache_behaviors|reverse|list }}"
+ purge_cache_behaviors: yes
+ state: present
+ <<: *aws_connection_info
+ register: reverse_cache_behaviors_with_purge
+
+ - name: check that reversing cache behaviors changes nothing when purge_cache_behaviors unset
+ assert:
+ that:
+ - reverse_cache_behaviors_with_purge.changed
+ - reverse_cache_behaviors_with_purge.cache_behaviors|length == 2
+
+ - name: update origin that changes target id (failure expected)
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}3.example.com"
+ id: "{{ test_identifier }}3.example.com"
+ purge_origins: yes
+ state: present
+ <<: *aws_connection_info
+ register: remove_origin_in_use
+ ignore_errors: yes
+
+ - name: check that removing in use origin fails
+ assert:
+ that:
+ - remove_origin_in_use.failed
+
+ # FIXME: This currently fails due to AWS side problems
+ # not clear whether to hope they fix or prevent this issue from happening
+ #- name: update origin and update cache behavior to point to new origin
+ # cloudfront_distribution:
+ # alias: "{{ cloudfront_alias }}"
+ # origins:
+ # - domain_name: "{{ test_identifier }}3.example.com"
+ # id: "{{ test_identifier }}3.example.com"
+ # cache_behaviors:
+ # - path_pattern: /test/path
+ # target_origin_id: "{{ test_identifier }}3.example.com"
+ # - path_pattern: /another/path
+ # target_origin_id: "{{ test_identifier }}3.example.com"
+ # state: present
+ # aws_access_key: "{{ aws_access_key|default(omit) }}"
+ # aws_secret_key: "{{ aws_secret_key|default(omit) }}"
+ # security_token: "{{ security_token|default(omit) }}"
+ # profile: "{{ profile|default(omit) }}"
+ # register: update_cache_behaviors in use
+
+ - name: create an s3 bucket for next test
+ aws_s3:
+ bucket: "{{ test_identifier }}-bucket"
+ mode: create
+ <<: *aws_connection_info
+
+ - name: update origin to point to the s3 bucket
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}-bucket.{{ aws_region }}.s3.amazonaws.com"
+ id: "{{ test_identifier }}3.example.com"
+ s3_origin_access_identity_enabled: yes
+ state: present
+ <<: *aws_connection_info
+ register: update_origin_to_s3
+
+ - name: check that s3 origin access is in result
+ assert:
+ that:
+ - item.s3_origin_config.origin_access_identity.startswith('origin-access-identity/cloudfront/')
+ when: "'s3_origin_config' in item"
+ loop: "{{ update_origin_to_s3.origins['items'] }}"
+
+ - name: update origin to remove s3 origin access identity
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}-bucket.{{ aws_region }}.s3.amazonaws.com"
+ id: "{{ test_identifier }}3.example.com"
+ s3_origin_access_identity_enabled: no
+ state: present
+ <<: *aws_connection_info
+ register: update_origin_to_s3_without_origin_access
+
+ - name: check that s3 origin access is not in result
+ assert:
+ that:
+ - not item.s3_origin_config.origin_access_identity
+ when: "'s3_origin_config' in item"
+ loop: "{{ update_origin_to_s3_without_origin_access.origins['items'] }}"
+
+ - name: delete the s3 bucket
+ aws_s3:
+ bucket: "{{ test_identifier }}-bucket"
+ mode: delete
+ <<: *aws_connection_info
+
+ - name: update origin to remove s3 origin access identity
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ origins:
+ - domain_name: "{{ test_identifier }}-bucket.{{ aws_region }}.s3.amazonaws.com"
+ id: "{{ test_identifier }}3.example.com"
+ s3_origin_access_identity_enabled: yes
+ custom_origin_config:
+ origin_protocol_policy: 'http-only'
+ state: present
+ <<: *aws_connection_info
+ register: update_origin_to_s3_with_origin_access_and_with_custom_origin_config
+ ignore_errors: True
+
+ - name: check that custom origin with origin access identity fails
+ assert:
+ that:
+ - update_origin_to_s3_with_origin_access_and_with_custom_origin_config.failed
+
+ always:
+ # TEARDOWN STARTS HERE
+ - name: clean up cloudfront distribution
+ cloudfront_distribution:
+ alias: "{{ cloudfront_alias }}"
+ enabled: no
+ wait: yes
+ state: absent
+ <<: *aws_connection_info