Feature - CloudFront (#4640)

This commit is contained in:
Bert Blommers 2021-11-29 13:35:43 -01:00 committed by GitHub
parent 41de9b82ac
commit 2cf37a4b90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1503 additions and 4 deletions

View File

@ -398,6 +398,104 @@
- [X] validate_template
</details>
## cloudfront
<details>
<summary>4% implemented</summary>
- [ ] associate_alias
- [ ] create_cache_policy
- [ ] create_cloud_front_origin_access_identity
- [X] create_distribution
- [ ] create_distribution_with_tags
- [ ] create_field_level_encryption_config
- [ ] create_field_level_encryption_profile
- [ ] create_function
- [ ] create_invalidation
- [ ] create_key_group
- [ ] create_monitoring_subscription
- [ ] create_origin_request_policy
- [ ] create_public_key
- [ ] create_realtime_log_config
- [ ] create_response_headers_policy
- [ ] create_streaming_distribution
- [ ] create_streaming_distribution_with_tags
- [ ] delete_cache_policy
- [ ] delete_cloud_front_origin_access_identity
- [X] delete_distribution
- [ ] delete_field_level_encryption_config
- [ ] delete_field_level_encryption_profile
- [ ] delete_function
- [ ] delete_key_group
- [ ] delete_monitoring_subscription
- [ ] delete_origin_request_policy
- [ ] delete_public_key
- [ ] delete_realtime_log_config
- [ ] delete_response_headers_policy
- [ ] delete_streaming_distribution
- [ ] describe_function
- [ ] get_cache_policy
- [ ] get_cache_policy_config
- [ ] get_cloud_front_origin_access_identity
- [ ] get_cloud_front_origin_access_identity_config
- [X] get_distribution
- [ ] get_distribution_config
- [ ] get_field_level_encryption
- [ ] get_field_level_encryption_config
- [ ] get_field_level_encryption_profile
- [ ] get_field_level_encryption_profile_config
- [ ] get_function
- [ ] get_invalidation
- [ ] get_key_group
- [ ] get_key_group_config
- [ ] get_monitoring_subscription
- [ ] get_origin_request_policy
- [ ] get_origin_request_policy_config
- [ ] get_public_key
- [ ] get_public_key_config
- [ ] get_realtime_log_config
- [ ] get_response_headers_policy
- [ ] get_response_headers_policy_config
- [ ] get_streaming_distribution
- [ ] get_streaming_distribution_config
- [ ] list_cache_policies
- [ ] list_cloud_front_origin_access_identities
- [ ] list_conflicting_aliases
- [X] list_distributions
- [ ] list_distributions_by_cache_policy_id
- [ ] list_distributions_by_key_group
- [ ] list_distributions_by_origin_request_policy_id
- [ ] list_distributions_by_realtime_log_config
- [ ] list_distributions_by_response_headers_policy_id
- [ ] list_distributions_by_web_acl_id
- [ ] list_field_level_encryption_configs
- [ ] list_field_level_encryption_profiles
- [ ] list_functions
- [ ] list_invalidations
- [ ] list_key_groups
- [ ] list_origin_request_policies
- [ ] list_public_keys
- [ ] list_realtime_log_configs
- [ ] list_response_headers_policies
- [ ] list_streaming_distributions
- [ ] list_tags_for_resource
- [ ] publish_function
- [ ] tag_resource
- [ ] test_function
- [ ] untag_resource
- [ ] update_cache_policy
- [ ] update_cloud_front_origin_access_identity
- [ ] update_distribution
- [ ] update_field_level_encryption_config
- [ ] update_field_level_encryption_profile
- [ ] update_function
- [ ] update_key_group
- [ ] update_origin_request_policy
- [ ] update_public_key
- [ ] update_realtime_log_config
- [ ] update_response_headers_policy
- [ ] update_streaming_distribution
</details>
## cloudtrail
<details>
<summary>44% implemented</summary>
@ -4711,7 +4809,6 @@
- cloud9
- cloudcontrol
- clouddirectory
- cloudfront
- cloudhsm
- cloudhsmv2
- cloudsearch

View File

@ -0,0 +1,134 @@
.. _implementedservice_cloudfront:
.. |start-h3| raw:: html
<h3>
.. |end-h3| raw:: html
</h3>
==========
cloudfront
==========
|start-h3| Example usage |end-h3|
.. sourcecode:: python
@mock_cloudfront
def test_cloudfront_behaviour:
boto3.client("cloudfront")
...
|start-h3| Implemented features for this service |end-h3|
- [ ] associate_alias
- [ ] create_cache_policy
- [ ] create_cloud_front_origin_access_identity
- [X] create_distribution
This has been tested against an S3-distribution with the simplest possible configuration.
Please raise an issue if we're not persisting/returning the correct attributes for your use-case.
- [ ] create_distribution_with_tags
- [ ] create_field_level_encryption_config
- [ ] create_field_level_encryption_profile
- [ ] create_function
- [ ] create_invalidation
- [ ] create_key_group
- [ ] create_monitoring_subscription
- [ ] create_origin_request_policy
- [ ] create_public_key
- [ ] create_realtime_log_config
- [ ] create_response_headers_policy
- [ ] create_streaming_distribution
- [ ] create_streaming_distribution_with_tags
- [ ] delete_cache_policy
- [ ] delete_cloud_front_origin_access_identity
- [X] delete_distribution
The IfMatch-value is ignored - any value is considered valid.
Calling this function without a value is invalid, per AWS' behaviour
- [ ] delete_field_level_encryption_config
- [ ] delete_field_level_encryption_profile
- [ ] delete_function
- [ ] delete_key_group
- [ ] delete_monitoring_subscription
- [ ] delete_origin_request_policy
- [ ] delete_public_key
- [ ] delete_realtime_log_config
- [ ] delete_response_headers_policy
- [ ] delete_streaming_distribution
- [ ] describe_function
- [ ] get_cache_policy
- [ ] get_cache_policy_config
- [ ] get_cloud_front_origin_access_identity
- [ ] get_cloud_front_origin_access_identity_config
- [X] get_distribution
- [ ] get_distribution_config
- [ ] get_field_level_encryption
- [ ] get_field_level_encryption_config
- [ ] get_field_level_encryption_profile
- [ ] get_field_level_encryption_profile_config
- [ ] get_function
- [ ] get_invalidation
- [ ] get_key_group
- [ ] get_key_group_config
- [ ] get_monitoring_subscription
- [ ] get_origin_request_policy
- [ ] get_origin_request_policy_config
- [ ] get_public_key
- [ ] get_public_key_config
- [ ] get_realtime_log_config
- [ ] get_response_headers_policy
- [ ] get_response_headers_policy_config
- [ ] get_streaming_distribution
- [ ] get_streaming_distribution_config
- [ ] list_cache_policies
- [ ] list_cloud_front_origin_access_identities
- [ ] list_conflicting_aliases
- [X] list_distributions
Pagination is not supported yet.
- [ ] list_distributions_by_cache_policy_id
- [ ] list_distributions_by_key_group
- [ ] list_distributions_by_origin_request_policy_id
- [ ] list_distributions_by_realtime_log_config
- [ ] list_distributions_by_response_headers_policy_id
- [ ] list_distributions_by_web_acl_id
- [ ] list_field_level_encryption_configs
- [ ] list_field_level_encryption_profiles
- [ ] list_functions
- [ ] list_invalidations
- [ ] list_key_groups
- [ ] list_origin_request_policies
- [ ] list_public_keys
- [ ] list_realtime_log_configs
- [ ] list_response_headers_policies
- [ ] list_streaming_distributions
- [ ] list_tags_for_resource
- [ ] publish_function
- [ ] tag_resource
- [ ] test_function
- [ ] untag_resource
- [ ] update_cache_policy
- [ ] update_cloud_front_origin_access_identity
- [ ] update_distribution
- [ ] update_field_level_encryption_config
- [ ] update_field_level_encryption_profile
- [ ] update_function
- [ ] update_key_group
- [ ] update_origin_request_policy
- [ ] update_public_key
- [ ] update_realtime_log_config
- [ ] update_response_headers_policy
- [ ] update_streaming_distribution

View File

@ -43,6 +43,7 @@ mock_cloudformation = lazy_load(".cloudformation", "mock_cloudformation")
mock_cloudformation_deprecated = lazy_load(
".cloudformation", "mock_cloudformation_deprecated"
)
mock_cloudfront = lazy_load(".cloudfront", "mock_cloudfront")
mock_cloudtrail = lazy_load(".cloudtrail", "mock_cloudtrail", boto3_name="cloudtrail")
mock_cloudwatch = lazy_load(".cloudwatch", "mock_cloudwatch")
mock_cloudwatch_deprecated = lazy_load(".cloudwatch", "mock_cloudwatch_deprecated")

View File

@ -13,6 +13,7 @@ backend_url_patterns = [
("batch", re.compile("https?://batch\\.(.+)\\.amazonaws.com")),
("budgets", re.compile("https?://budgets\\.amazonaws\\.com")),
("cloudformation", re.compile("https?://cloudformation\\.(.+)\\.amazonaws\\.com")),
("cloudfront", re.compile("https?://cloudfront\\.amazonaws\\.com")),
("cloudtrail", re.compile("https?://cloudtrail\\.(.+)\\.amazonaws\\.com")),
("cloudwatch", re.compile("https?://monitoring\\.(.+)\\.amazonaws.com")),
("codecommit", re.compile("https?://codecommit\\.(.+)\\.amazonaws\\.com")),

View File

@ -0,0 +1,5 @@
from .models import cloudfront_backend
from ..core.models import base_decorator
cloudfront_backends = {"global": cloudfront_backend}
mock_cloudfront = base_decorator(cloudfront_backends)

View File

@ -0,0 +1,81 @@
from moto.core.exceptions import RESTError
EXCEPTION_RESPONSE = """<?xml version="1.0"?>
<ErrorResponse xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
<Error>
<Type>Sender</Type>
<Code>{{ error_type }}</Code>
<Message>{{ message }}</Message>
</Error>
<{{ request_id_tag }}>30c0dedb-92b1-4e2b-9be4-1188e3ed86ab</{{ request_id_tag }}>
</ErrorResponse>"""
class CloudFrontException(RESTError):
code = 400
def __init__(self, *args, **kwargs):
kwargs.setdefault("template", "cferror")
self.templates["cferror"] = EXCEPTION_RESPONSE
super().__init__(*args, **kwargs)
class OriginDoesNotExist(CloudFrontException):
code = 404
def __init__(self, **kwargs):
super().__init__(
"NoSuchOrigin",
message="One or more of your origins or origin groups do not exist.",
**kwargs,
)
class InvalidOriginServer(CloudFrontException):
def __init__(self, **kwargs):
super().__init__(
"InvalidOrigin",
message="The specified origin server does not exist or is not valid.",
**kwargs,
)
class DomainNameNotAnS3Bucket(CloudFrontException):
def __init__(self, **kwargs):
super().__init__(
"InvalidArgument",
message="The parameter Origin DomainName does not refer to a valid S3 bucket.",
**kwargs,
)
class DistributionAlreadyExists(CloudFrontException):
def __init__(self, dist_id, **kwargs):
super().__init__(
"DistributionAlreadyExists",
message=f"The caller reference that you are using to create a distribution is associated with another distribution. Already exists: {dist_id}",
**kwargs,
)
class InvalidIfMatchVersion(CloudFrontException):
def __init__(self, **kwargs):
super().__init__(
"InvalidIfMatchVersion",
message="The If-Match version is missing or not valid for the resource.",
**kwargs,
)
class NoSuchDistribution(CloudFrontException):
code = 404
def __init__(self, **kwargs):
super().__init__(
"NoSuchDistribution",
message="The specified distribution does not exist.",
**kwargs,
)

224
moto/cloudfront/models.py Normal file
View File

@ -0,0 +1,224 @@
import random
import string
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel
from uuid import uuid4
from .exceptions import (
OriginDoesNotExist,
InvalidOriginServer,
DomainNameNotAnS3Bucket,
DistributionAlreadyExists,
InvalidIfMatchVersion,
NoSuchDistribution,
)
class ActiveTrustedSigners:
def __init__(self):
self.enabled = False
self.quantity = 0
self.signers = []
class ActiveTrustedKeyGroups:
def __init__(self):
self.enabled = False
self.quantity = 0
self.kg_key_pair_ids = []
class LambdaFunctionAssociation:
def __init__(self):
self.arn = ""
self.event_type = ""
self.include_body = False
class ForwardedValues:
def __init__(self):
self.query_string = ""
self.whitelisted_names = []
self.headers = []
self.query_string_cache_keys = []
class DefaultCacheBehaviour:
def __init__(self, config):
self.target_origin_id = config["TargetOriginId"]
self.trusted_signers_enabled = False
self.trusted_signers = []
self.trusted_key_groups_enabled = False
self.trusted_key_groups = []
self.viewer_protocol_policy = config["ViewerProtocolPolicy"]
self.allowed_methods = ["HEAD", "GET"]
self.cached_methods = ["GET", "HEAD"]
self.smooth_streaming = True
self.compress = True
self.lambda_function_associations = []
self.function_associations = []
self.field_level_encryption_id = ""
self.forwarded_values = ForwardedValues()
self.min_ttl = 0
self.default_ttl = 0
self.max_ttl = 0
class Logging:
def __init__(self):
self.enabled = False
class ViewerCertificate:
def __init__(self):
self.cloud_front_default_certificate = True
self.min_protocol_version = "TLSv1"
self.certificate_source = "cloudfront"
class Origin:
def __init__(self, origin):
self.id = origin["Id"]
self.domain_name = origin["DomainName"]
self.custom_headers = []
self.s3_access_identity = ""
self.custom_origin = None
self.origin_shield = None
self.connection_attempts = 3
self.connection_timeout = 10
if "S3OriginConfig" not in origin and "CustomOriginConfig" not in origin:
raise InvalidOriginServer
if "S3OriginConfig" in origin:
# Very rough validation
if not self.domain_name.endswith("amazonaws.com"):
raise DomainNameNotAnS3Bucket
self.s3_access_identity = origin["S3OriginConfig"]["OriginAccessIdentity"]
class DistributionConfig:
def __init__(self, config):
self.config = config
self.default_cache_behavior = DefaultCacheBehaviour(
config["DefaultCacheBehavior"]
)
self.cache_behaviors = []
self.custom_error_responses = []
self.logging = Logging()
self.enabled = False
self.viewer_certificate = ViewerCertificate()
self.geo_restriction_type = "none"
self.geo_restrictions = []
self.caller_reference = config.get("CallerReference", str(uuid4()))
self.origins = config["Origins"]["Items"]["Origin"]
if not isinstance(self.origins, list):
self.origins = [self.origins]
# This check happens before any other Origins-validation
if self.default_cache_behavior.target_origin_id not in [
o["Id"] for o in self.origins
]:
raise OriginDoesNotExist
self.origins = [Origin(o) for o in self.origins]
self.price_class = "PriceClass_All"
self.http_version = "http2"
self.is_ipv6_enabled = True
class Distribution(BaseModel):
@staticmethod
def random_id(uppercase=True):
ascii_set = string.ascii_uppercase if uppercase else string.ascii_lowercase
chars = list(range(10)) + list(ascii_set)
resource_id = random.choice(ascii_set) + "".join(
str(random.choice(chars)) for _ in range(12)
)
return resource_id
def __init__(self, config):
self.distribution_id = Distribution.random_id()
self.arn = (
f"arn:aws:cloudfront:{ACCOUNT_ID}:distribution/{self.distribution_id}"
)
self.distribution_config = DistributionConfig(config)
self.active_trusted_signers = ActiveTrustedSigners()
self.active_trusted_key_groups = ActiveTrustedKeyGroups()
self.aliases = []
self.origin_groups = []
self.alias_icp_recordals = []
self.last_modified_time = "2021-11-27T10:34:26.802Z"
self.in_progress_invalidation_batches = 0
self.has_active_trusted_key_groups = False
self.status = "InProgress"
self.domain_name = f"{Distribution.random_id(uppercase=False)}.cloudfront.net"
def advance(self):
"""
Advance the status of this Distribution, to mimick AWS' behaviour
"""
if self.status == "InProgress":
self.status = "Deployed"
@property
def location(self):
return f"https://cloudfront.amazonaws.com/2020-05-31/distribution/{self.distribution_id}"
@property
def etag(self):
return Distribution.random_id()
class CloudFrontBackend(BaseBackend):
def __init__(self):
self.distributions = dict()
def create_distribution(self, distribution_config):
"""
This has been tested against an S3-distribution with the simplest possible configuration.
Please raise an issue if we're not persisting/returning the correct attributes for your use-case.
"""
dist = Distribution(distribution_config)
caller_reference = dist.distribution_config.caller_reference
existing_dist = self._distribution_with_caller_reference(caller_reference)
if existing_dist:
raise DistributionAlreadyExists(existing_dist.distribution_id)
self.distributions[dist.distribution_id] = dist
return dist, dist.location, dist.etag
def get_distribution(self, distribution_id):
if distribution_id not in self.distributions:
raise NoSuchDistribution
dist = self.distributions[distribution_id]
dist.advance()
return dist, dist.etag
def delete_distribution(self, distribution_id, if_match):
"""
The IfMatch-value is ignored - any value is considered valid.
Calling this function without a value is invalid, per AWS' behaviour
"""
if not if_match:
raise InvalidIfMatchVersion
if distribution_id not in self.distributions:
raise NoSuchDistribution
del self.distributions[distribution_id]
def list_distributions(self):
"""
Pagination is not supported yet.
"""
for dist in self.distributions.values():
dist.advance()
return self.distributions.values()
def _distribution_with_caller_reference(self, reference):
for dist in self.distributions.values():
config = dist.distribution_config
if config.caller_reference == reference:
return dist
return False
cloudfront_backend = CloudFrontBackend()

View File

@ -0,0 +1,514 @@
import xmltodict
from functools import wraps
from moto.core.responses import BaseResponse
from .models import cloudfront_backend
from .exceptions import CloudFrontException
XMLNS = "http://cloudfront.amazonaws.com/doc/2020-05-31/"
def error_handler(f):
@wraps(f)
def _wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except CloudFrontException as e:
return e.code, e.get_headers(), e.get_body()
return _wrapper
class CloudFrontResponse(BaseResponse):
def _get_xml_body(self):
return xmltodict.parse(self.body, dict_constructor=dict)
@error_handler
def distributions(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
if request.method == "POST":
return self.create_distribution()
if request.method == "GET":
return self.list_distributions()
def create_distribution(self):
params = self._get_xml_body()
distribution_config = params.get("DistributionConfig")
distribution, location, e_tag = cloudfront_backend.create_distribution(
distribution_config=distribution_config,
)
template = self.response_template(CREATE_DISTRIBUTION_TEMPLATE)
response = template.render(distribution=distribution, xmlns=XMLNS)
headers = {"ETag": e_tag, "Location": location}
return 200, headers, response
def list_distributions(self):
distributions = cloudfront_backend.list_distributions()
template = self.response_template(LIST_TEMPLATE)
response = template.render(distributions=distributions)
return 200, {}, response
@error_handler
def individual_distribution(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
distribution_id = full_url.split("/")[-1]
if request.method == "DELETE":
if_match = self._get_param("If-Match")
cloudfront_backend.delete_distribution(distribution_id, if_match)
return 204, {}, ""
if request.method == "GET":
dist, etag = cloudfront_backend.get_distribution(distribution_id)
template = self.response_template(GET_DISTRIBUTION_TEMPLATE)
response = template.render(distribution=dist, xmlns=XMLNS)
return 200, {"ETag": etag}, response
DIST_META_TEMPLATE = """
<Id>{{ distribution.distribution_id }}</Id>
<ARN>{{ distribution.arn }}</ARN>
<Status>{{ distribution.status }}</Status>
<LastModifiedTime>{{ distribution.last_modified_time }}</LastModifiedTime>
<InProgressInvalidationBatches>{{ distribution.in_progress_invalidation_batches }}</InProgressInvalidationBatches>
<DomainName>{{ distribution.domain_name }}</DomainName>
"""
DIST_CONFIG_TEMPLATE = """
<CallerReference>{{ distribution.distribution_config.caller_reference }}</CallerReference>
<Aliases>
<Quantity>{{ distribution.distribution_config.aliases|length }}</Quantity>
<Items>
{% for alias in distribution.distribution_config.aliases %}
<CNAME>{{ alias }}</CNAME>
{% endfor %}
</Items>
</Aliases>
<DefaultRootObject>{{ distribution.distribution_config.default_distribution_object }}</DefaultRootObject>
<Origins>
<Quantity>{{ distribution.distribution_config.origins|length }}</Quantity>
<Items>
{% for origin in distribution.distribution_config.origins %}
<Origin>
<Id>{{ origin.id }}</Id>
<DomainName>{{ origin.domain_name }}</DomainName>
<OriginPath>{{ origin.origin_path }}</OriginPath>
<CustomHeaders>
<Quantity>{{ origin.custom_headers|length }}</Quantity>
<Items>
{% for header in origin.custom_headers %}
<HeaderName>{{ header.header_name }}</HeaderName>
<HeaderValue>{{ header.header_value }}</HeaderValue>
{% endfor %}
</Items>
</CustomHeaders>
<S3OriginConfig>
<OriginAccessIdentity>{{ origin.s3_access_identity }}</OriginAccessIdentity>
</S3OriginConfig>
{% if origin.custom_origin %}
<CustomOriginConfig>
<HTTPPort>{{ origin.custom_origin.http_port }}</HTTPPort>
<HTTPSPort>{{ origin.custom_origin.https_port }}</HTTPSPort>
<OriginProtocolPolicy>{{ OriginProtocolPolicy }}</OriginProtocolPolicy>
<OriginSslProtocols>
<Quantity>{{ origin.custom_origin.origin_ssl_protocols.quantity }}</Quantity>
<Items>
{% for protocol in origin.custom_origin.origin_ssl_protocols %}
{{ protocol }}
{% endfor %}
</Items>
</OriginSslProtocols>
<OriginReadTimeout>{{ origin.custom_origin.origin_read_timeout }}</OriginReadTimeout>
<OriginKeepaliveTimeout>{{ origin.custom_origin.origin_keepalive_timeout }}</OriginKeepaliveTimeout>
</CustomOriginConfig>
{% endif %}
<ConnectionAttempts>{{ origin.connection_attempts }}</ConnectionAttempts>
<ConnectionTimeout>{{ origin.connection_timeout }}</ConnectionTimeout>
{% if origin.origin_shield %}
<OriginShield>
<Enabled>{{ origin.origin_shield.enabled }}</Enabled>
<OriginShieldRegion>{{ OriginShieldRegion }}</OriginShieldRegion>
</OriginShield>
{% else %}
<OriginShield>
<Enabled>false</Enabled>
</OriginShield>
{% endif %}
</Origin>
{% endfor %}
</Items>
</Origins>
<OriginGroups>
<Quantity>{{ distribution.distribution_config.origin_groups|length }}</Quantity>
{% if distribution.distribution_config.origin_groups %}
<Items>
{% for origin_group in distribution.distribution_config.origin_groups %}
<Id>{{ origin_group.id }}</Id>
<FailoverCriteria>
<StatusCodes>
<Quantity>{{ origin_group.failover_criteria.status_codes.quantity }}</Quantity>
<Items>
{% for status_code_list in origin_group_list.failover_criteria.status_codes.StatusCodeList %}
<StatusCode>{{ status_code_list.status_code }}</StatusCode>
{% endfor %}
</Items>
</StatusCodes>
</FailoverCriteria>
<Members>
<Quantity>{{ origin_group.members.quantity }}</Quantity>
<Items>
{% for origin_group_member_list in origin_group.members.OriginGroupMemberList %}
<OriginId>{{ origin_group_member_list.origin_id }}</OriginId>
{% endfor %}
</Items>
</Members>
{% endfor %}
</Items>
{% endif %}
</OriginGroups>
<DefaultCacheBehavior>
<TargetOriginId>{{ distribution.distribution_config.default_cache_behavior.target_origin_id }}</TargetOriginId>
<TrustedSigners>
<Enabled>{{ distribution.distribution_config.default_cache_behavior.trusted_signers.enabled }}</Enabled>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.trusted_signers|length }}</Quantity>
<Items>
{% for aws_account_number in distribution.distribution_config.default_cache_behavior.trusted_signers %}
<AwsAccountNumber>{{ aws_account_number }}</AwsAccountNumber>
{% endfor %}
</Items>
</TrustedSigners>
<TrustedKeyGroups>
<Enabled>{{ distribution.distribution_config.default_cache_behavior.trusted_key_groups_enabled }}</Enabled>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.trusted_key_groups|length }}</Quantity>
<Items>
{% for key_group in distribution.distribution_config.default_cache_behavior.trusted_key_groups %}
<KeyGroup>{{ key_group }}</KeyGroup>
{% endfor %}
</Items>
</TrustedKeyGroups>
<ViewerProtocolPolicy>{{ distribution.distribution_config.default_cache_behavior.viewer_protocol_policy }}</ViewerProtocolPolicy>
<AllowedMethods>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.allowed_methods|length }}</Quantity>
<Items>
{% for method in distribution.distribution_config.default_cache_behavior.allowed_methods %}
<member>{{ method }}</member>
{% endfor %}
</Items>
<CachedMethods>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.cached_methods|length }}</Quantity>
<Items>
{% for method in distribution.distribution_config.default_cache_behavior.cached_methods %}
<member>{{ method }}</member>
{% endfor %}
</Items>
</CachedMethods>
</AllowedMethods>
<SmoothStreaming>{{ distribution.distribution_config.default_cache_behavior.smooth_streaming }}</SmoothStreaming>
<Compress>{{ 'true' if distribution.distribution_config.default_cache_behavior.compress else 'false' }}</Compress>
<LambdaFunctionAssociations>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.lambda_function_associations|length }}</Quantity>
{% if distribution.distribution_config.default_cache_behavior.lambda_function_associations %}
<Items>
{% for func in distribution.distribution_config.default_cache_behavior.lambda_function_associations %}
<LambdaFunctionARN>{{ func.arn }}</LambdaFunctionARN>
<EventType>{{ func.event_type }}</EventType>
<IncludeBody>{{ func.include_body }}</IncludeBody>
{% endfor %}
</Items>
{% endif %}
</LambdaFunctionAssociations>
<FunctionAssociations>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.function_associations|length }}</Quantity>
{% if distribution.distribution_config.default_cache_behavior.function_associations %}
<Items>
{% for func in distribution.distribution_config.default_cache_behavior.function_associations %}
<FunctionARN>{{ func.arn }}</FunctionARN>
<EventType>{{ func.event_type }}</EventType>
{% endfor %}
</Items>
{% endif %}
</FunctionAssociations>
<FieldLevelEncryptionId>{{ distribution.distribution_config.default_cache_behavior.field_level_encryption_id }}</FieldLevelEncryptionId>
<RealtimeLogConfigArn>{{ distribution.distribution_config.default_cache_behavior.realtime_log_config_arn }}</RealtimeLogConfigArn>
<CachePolicyId>{{ distribution.distribution_config.default_cache_behavior.cache_policy_id }}</CachePolicyId>
<OriginRequestPolicyId>{{ distribution.distribution_config.default_cache_behavior.origin_request_policy_id }}</OriginRequestPolicyId>
<ResponseHeadersPolicyId>{{ distribution.distribution_config.default_cache_behavior.response_headers_policy_id }}</ResponseHeadersPolicyId>
<ForwardedValues>
<QueryString>{{ distribution.distribution_config.default_cache_behavior.forwarded_values.query_string }}</QueryString>
<Cookies>
<Forward>{{ ItemSelection }}</Forward>
<WhitelistedNames>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.forwarded_values.whitelisted_names|length }}</Quantity>
<Items>
{% for name in distribution.distribution_config.default_cache_behavior.forwarded_values.whitelisted_names %}
<Name>{{ name }}</Name>
{% endfor %}
</Items>
</WhitelistedNames>
</Cookies>
<Headers>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.forwarded_values.headers|length }}</Quantity>
<Items>
{% for h in distribution.distribution_config.default_cache_behavior.forwarded_values.headers %}
<Name>{{ h }}</Name>
{% endfor %}
</Items>
</Headers>
<QueryStringCacheKeys>
<Quantity>{{ distribution.distribution_config.default_cache_behavior.forwarded_values.query_string_cache_keys|length }}</Quantity>
<Items>
{% for key in distribution.distribution_config.default_cache_behavior.forwarded_values.query_string_cache_keys %}
<Name>{{ key }}</Name>
{% endfor %}
</Items>
</QueryStringCacheKeys>
</ForwardedValues>
<MinTTL>{{ distribution.distribution_config.default_cache_behavior.min_ttl }}</MinTTL>
<DefaultTTL>{{ distribution.distribution_config.default_cache_behavior.default_ttl }}</DefaultTTL>
<MaxTTL>{{ distribution.distribution_config.default_cache_behavior.max_ttl }}</MaxTTL>
</DefaultCacheBehavior>
<CacheBehaviors>
<Quantity>{{ distribution.distribution_config.cache_behaviors|length }}</Quantity>
{% if distribution.distribution_config.cache_behaviors %}
<Items>
{% for behaviour in distribution.distribution_config.cache_behaviors %}
<PathPattern>{{ behaviour.path_pattern }}</PathPattern>
<TargetOriginId>{{ behaviour.target_origin_id }}</TargetOriginId>
<TrustedSigners>
<Enabled>{{ behaviour.trusted_signers.enabled }}</Enabled>
<Quantity>{{ behaviour.trusted_signers.quantity }}</Quantity>
<Items>
{% for account_nr in behaviour.trusted_signers %}
<AwsAccountNumber>{{ account_nr }}</AwsAccountNumber>
{% endfor %}
</Items>
</TrustedSigners>
<TrustedKeyGroups>
<Enabled>{{ cache_behavior_list.trusted_key_groups.enabled }}</Enabled>
<Quantity>{{ cache_behavior_list.trusted_key_groups.quantity }}</Quantity>
<Items>
{% for trusted_key_group_id_list in cache_behavior_list.trusted_key_groups.TrustedKeyGroupIdList %}
<KeyGroup>{{ trusted_key_group_id_list.key_group }}</KeyGroup>
{% endfor %}
</Items>
</TrustedKeyGroups>
<ViewerProtocolPolicy>{{ ViewerProtocolPolicy }}</ViewerProtocolPolicy>
<AllowedMethods>
<Quantity>{{ cache_behavior_list.allowed_methods.quantity }}</Quantity>
<Items>
{% for methods_list in cache_behavior_list.allowed_methods.MethodsList %}{{ Method }}{% endfor %}
</Items>
<CachedMethods>
<Quantity>{{ cache_behavior_list.allowed_methods.cached_methods.quantity }}</Quantity>
<Items>
{% for methods_list in cache_behavior_list.allowed_methods.cached_methods.MethodsList %}{{ Method }}{% endfor %}
</Items>
</CachedMethods>
</AllowedMethods>
<SmoothStreaming>{{ cache_behavior_list.smooth_streaming }}</SmoothStreaming>
<Compress>{{ cache_behavior_list.compress }}</Compress>
<LambdaFunctionAssociations>
<Quantity>{{ cache_behavior_list.lambda_function_associations.quantity }}</Quantity>
<Items>
{% for lambda_function_association_list in cache_behavior_list.lambda_function_associations.LambdaFunctionAssociationList %}
<LambdaFunctionARN>{{ LambdaFunctionARN }}</LambdaFunctionARN>
<EventType>{{ EventType }}</EventType>
<IncludeBody>{{ lambda_function_association_list.include_body }}</IncludeBody>
{% endfor %}
</Items>
</LambdaFunctionAssociations>
<FunctionAssociations>
<Quantity>{{ cache_behavior_list.function_associations.quantity }}</Quantity>
<Items>
{% for function_association_list in cache_behavior_list.function_associations.FunctionAssociationList %}
<FunctionARN>{{ FunctionARN }}</FunctionARN>
<EventType>{{ EventType }}</EventType>
{% endfor %}
</Items>
</FunctionAssociations>
<FieldLevelEncryptionId>{{ cache_behavior_list.field_level_encryption_id }}</FieldLevelEncryptionId>
<RealtimeLogConfigArn>{{ cache_behavior_list.realtime_log_config_arn }}</RealtimeLogConfigArn>
<CachePolicyId>{{ cache_behavior_list.cache_policy_id }}</CachePolicyId>
<OriginRequestPolicyId>{{ cache_behavior_list.origin_request_policy_id }}</OriginRequestPolicyId>
<ResponseHeadersPolicyId>{{ cache_behavior_list.response_headers_policy_id }}</ResponseHeadersPolicyId>
<ForwardedValues>
<QueryString>{{ cache_behavior_list.forwarded_values.query_string }}</QueryString>
<Cookies>
<Forward>{{ ItemSelection }}</Forward>
<WhitelistedNames>
<Quantity>{{ cache_behavior_list.forwarded_values.cookies.whitelisted_names.quantity }}</Quantity>
<Items>
{% for cookie_name_list in cache_behavior_list.forwarded_values.cookies.whitelisted_names.CookieNameList %}
<Name>{{ cookie_name_list.name }}</Name>
{% endfor %}
</Items>
</WhitelistedNames>
</Cookies>
<Headers>
<Quantity>{{ cache_behavior_list.forwarded_values.headers.quantity }}</Quantity>
<Items>
{% for header_list in cache_behavior_list.forwarded_values.headers.HeaderList %}
<Name>{{ header_list.name }}</Name>
{% endfor %}
</Items>
</Headers>
<QueryStringCacheKeys>
<Quantity>{{ cache_behavior_list.forwarded_values.query_string_cache_keys.quantity }}</Quantity>
<Items>
{% for query_string_cache_keys_list in cache_behavior_list.forwarded_values.query_string_cache_keys.QueryStringCacheKeysList %}
<Name>{{ query_string_cache_keys_list.name }}</Name>
{% endfor %}
</Items>
</QueryStringCacheKeys>
</ForwardedValues>
<MinTTL>{{ cache_behavior_list.min_ttl }}</MinTTL>
<DefaultTTL>{{ cache_behavior_list.default_ttl }}</DefaultTTL>
<MaxTTL>{{ cache_behavior_list.max_ttl }}</MaxTTL>
{% endfor %}
</Items>
{% endif %}
</CacheBehaviors>
<CustomErrorResponses>
<Quantity>{{ distribution.distribution_config.custom_error_responses|length }}</Quantity>
{% if distribution.distribution_config.custom_error_responses %}
<Items>
{% for response in distribution.distribution_config.custom_error_responses %}
<ErrorCode>{{ response.error_code }}</ErrorCode>
<ResponsePagePath>{{ response.response_page_path }}</ResponsePagePath>
<ResponseCode>{{ response.response_code }}</ResponseCode>
<ErrorCachingMinTTL>{{ response.error_caching_min_ttl }}</ErrorCachingMinTTL>
{% endfor %}
</Items>
{% endif %}
</CustomErrorResponses>
<Comment>{{ CommentType }}</Comment>
<Logging>
<Enabled>{{ distribution.distribution_config.logging.enabled }}</Enabled>
<IncludeCookies>{{ distribution.distribution_config.logging.include_cookies }}</IncludeCookies>
<Bucket>{{ distribution.distribution_config.logging.bucket }}</Bucket>
<Prefix>{{ distribution.distribution_config.logging.prefix }}</Prefix>
</Logging>
<PriceClass>{{ distribution.distribution_config.price_class }}</PriceClass>
<Enabled>{{ distribution.distribution_config.enabled }}</Enabled>
<ViewerCertificate>
<CloudFrontDefaultCertificate>{{ 'true' if distribution.distribution_config.viewer_certificate.cloud_front_default_certificate else 'false' }}</CloudFrontDefaultCertificate>
<IAMCertificateId>{{ distribution.distribution_config.viewer_certificate.iam_certificate_id }}</IAMCertificateId>
<ACMCertificateArn>{{ distribution.distribution_config.viewer_certificate.acm_certificate_arn }}</ACMCertificateArn>
<SSLSupportMethod>{{ SSLSupportMethod }}</SSLSupportMethod>
<MinimumProtocolVersion>{{ distribution.distribution_config.viewer_certificate.min_protocol_version }}</MinimumProtocolVersion>
<Certificate>{{ distribution.distribution_config.viewer_certificate.certificate }}</Certificate>
<CertificateSource>{{ distribution.distribution_config.viewer_certificate.certificate_source }}</CertificateSource>
</ViewerCertificate>
<Restrictions>
<GeoRestriction>
<RestrictionType>{{ distribution.distribution_config.geo_restriction_type }}</RestrictionType>
<Quantity>{{ distribution.distribution_config.geo_restrictions|length }}</Quantity>
{% if distribution.distribution_config.geo_restrictions %}
<Items>
{% for location in distribution.distribution_config.geo_restrictions %}
<Location>{{ location }}</Location>
{% endfor %}
</Items>
{% endif %}
</GeoRestriction>
</Restrictions>
<WebACLId>{{ distribution.distribution_config.web_acl_id }}</WebACLId>
<HttpVersion>{{ distribution.distribution_config.http_version }}</HttpVersion>
<IsIPV6Enabled>{{ 'true' if distribution.distribution_config.is_ipv6_enabled else 'false' }}</IsIPV6Enabled>
"""
DISTRIBUTION_TEMPLATE = (
DIST_META_TEMPLATE
+ """
<ActiveTrustedSigners>
<Enabled>{{ distribution.active_trusted_signers.enabled }}</Enabled>
<Quantity>{{ distribution.active_trusted_signers.quantity }}</Quantity>
<Items>
{% for signer in distribution.active_trusted_signers.signers %}
<AwsAccountNumber>{{ signer.aws_account_number }}</AwsAccountNumber>
<KeyPairIds>
<Quantity>{{ signer.key_pair_ids.quantity }}</Quantity>
<Items>
{% for key_pair_id_list in signer.key_pair_ids.KeyPairIdList %}
<KeyPairId>{{ key_pair_id_list.key_pair_id }}</KeyPairId>
{% endfor %}
</Items>
</KeyPairIds>
{% endfor %}
</Items>
</ActiveTrustedSigners>
<ActiveTrustedKeyGroups>
<Enabled>{{ distribution.active_trusted_key_groups.enabled }}</Enabled>
<Quantity>{{ distribution.active_trusted_key_groups.quantity }}</Quantity>
<Items>
{% for kg_key_pair_id in distribution.active_trusted_key_groups.kg_key_pair_ids %}
<KeyGroupId>{{ kg_key_pair_id.key_group_id }}</KeyGroupId>
<KeyPairIds>
<Quantity>{{ kg_key_pair_id.key_pair_ids.quantity }}</Quantity>
<Items>
{% for key_pair_id_list in kg_key_pair_ids_list.key_pair_ids.KeyPairIdList %}
<KeyPairId>{{ key_pair_id_list.key_pair_id }}</KeyPairId>
{% endfor %}
</Items>
</KeyPairIds>
{% endfor %}
</Items>
</ActiveTrustedKeyGroups>
<DistributionConfig>
"""
+ DIST_CONFIG_TEMPLATE
+ """
</DistributionConfig>
<AliasICPRecordals>
{% for a in distribution.alias_icp_recordals %}
<CNAME>{{ a.cname }}</CNAME>
<ICPRecordalStatus>{{ a.status }}</ICPRecordalStatus>
{% endfor %}
</AliasICPRecordals>"""
)
CREATE_DISTRIBUTION_TEMPLATE = (
"""<?xml version="1.0"?>
<CreateDistributionResult xmlns="{{ xmlns }}">
"""
+ DISTRIBUTION_TEMPLATE
+ """
</CreateDistributionResult>
"""
)
GET_DISTRIBUTION_TEMPLATE = (
"""<?xml version="1.0"?>
<Distribution xmlns="{{ xmlns }}">
"""
+ DISTRIBUTION_TEMPLATE
+ """
</Distribution>
"""
)
LIST_TEMPLATE = (
"""<?xml version="1.0"?>
<DistributionList xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
<Marker></Marker>
<MaxItems>100</MaxItems>
<IsTruncated>false</IsTruncated>
<Quantity>{{ distributions|length }}</Quantity>
{% if distributions %}
<Items>
{% for distribution in distributions %}
<DistributionSummary>
"""
+ DIST_META_TEMPLATE
+ """
"""
+ DIST_CONFIG_TEMPLATE
+ """
</DistributionSummary>
{% endfor %}
</Items>
{% endif %}
</DistributionList>"""
)

13
moto/cloudfront/urls.py Normal file
View File

@ -0,0 +1,13 @@
"""cloudfront base URL and path."""
from .responses import CloudFrontResponse
response = CloudFrontResponse()
url_bases = [
r"https?://cloudfront\.amazonaws\.com",
]
url_paths = {
"{0}/2020-05-31/distribution$": response.distributions,
"{0}/2020-05-31/distribution/(?P<distribution_id>[^/]+)$": response.individual_distribution,
}

View File

@ -144,7 +144,7 @@ class convert_flask_to_httpretty_response(object):
from flask import request, Response
try:
result = self.callback(request, request.url, {})
result = self.callback(request, request.url, dict(request.headers))
except ClientError as exc:
result = 400, {}, exc.response["Error"]["Message"]
# result is a status, headers, response tuple

View File

@ -119,8 +119,8 @@ class DomainDispatcherApplication(object):
# S3 is the last resort when the target is also unknown
service, region = DEFAULT_SERVICE_REGION
if service == "budgets":
# Budgets is global
if service in ["budgets", "cloudfront"]:
# Global Services - they do not have/expect a region
host = f"{service}.amazonaws.com"
elif service == "mediastore" and not target:
# All MediaStore API calls have a target header

View File

View File

@ -0,0 +1,415 @@
import boto3
import pytest
import sure # noqa # pylint: disable=unused-import
from botocore.exceptions import ClientError
from moto import mock_cloudfront
from moto.core import ACCOUNT_ID
def example_distribution_config(ref):
return {
"CallerReference": ref,
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "origin1",
"DomainName": "asdf.s3.us-east-1.amazonaws.com",
"S3OriginConfig": {"OriginAccessIdentity": ""},
}
],
},
"DefaultCacheBehavior": {
"TargetOriginId": "origin1",
"ViewerProtocolPolicy": "allow-all",
"MinTTL": 10,
"ForwardedValues": {"QueryString": False, "Cookies": {"Forward": "none",}},
},
"Comment": "an optional comment that's not actually optional",
"Enabled": False,
}
@mock_cloudfront
def test_create_distribution_s3_minimum():
client = boto3.client("cloudfront", region_name="us-west-1")
config = example_distribution_config("ref")
resp = client.create_distribution(DistributionConfig=config)
resp.should.have.key("Distribution")
distribution = resp["Distribution"]
distribution.should.have.key("Id")
distribution.should.have.key("ARN").equals(
f"arn:aws:cloudfront:{ACCOUNT_ID}:distribution/{distribution['Id']}"
)
distribution.should.have.key("Status").equals("InProgress")
distribution.should.have.key("LastModifiedTime")
distribution.should.have.key("InProgressInvalidationBatches").equals(0)
distribution.should.have.key("DomainName").should.contain(".cloudfront.net")
distribution.should.have.key("ActiveTrustedSigners")
signers = distribution["ActiveTrustedSigners"]
signers.should.have.key("Enabled").equals(False)
signers.should.have.key("Quantity").equals(0)
distribution.should.have.key("ActiveTrustedKeyGroups")
key_groups = distribution["ActiveTrustedKeyGroups"]
key_groups.should.have.key("Enabled").equals(False)
key_groups.should.have.key("Quantity").equals(0)
distribution.should.have.key("DistributionConfig")
config = distribution["DistributionConfig"]
config.should.have.key("CallerReference").should.equal("ref")
config.should.have.key("Aliases")
config["Aliases"].should.have.key("Quantity").equals(0)
config.should.have.key("Origins")
origins = config["Origins"]
origins.should.have.key("Quantity").equals(1)
origins.should.have.key("Items").length_of(1)
origin = origins["Items"][0]
origin.should.have.key("Id").equals("origin1")
origin.should.have.key("DomainName").equals("asdf.s3.us-east-1.amazonaws.com")
origin.should.have.key("OriginPath").equals("")
origin.should.have.key("CustomHeaders")
origin["CustomHeaders"].should.have.key("Quantity").equals(0)
origin.should.have.key("ConnectionAttempts").equals(3)
origin.should.have.key("ConnectionTimeout").equals(10)
origin.should.have.key("OriginShield").equals({"Enabled": False})
config.should.have.key("OriginGroups").equals({"Quantity": 0})
config.should.have.key("DefaultCacheBehavior")
default_cache = config["DefaultCacheBehavior"]
default_cache.should.have.key("TargetOriginId").should.equal("origin1")
default_cache.should.have.key("TrustedSigners")
signers = default_cache["TrustedSigners"]
signers.should.have.key("Enabled").equals(False)
signers.should.have.key("Quantity").equals(0)
default_cache.should.have.key("TrustedKeyGroups")
groups = default_cache["TrustedKeyGroups"]
groups.should.have.key("Enabled").equals(False)
groups.should.have.key("Quantity").equals(0)
default_cache.should.have.key("ViewerProtocolPolicy").equals("allow-all")
default_cache.should.have.key("AllowedMethods")
methods = default_cache["AllowedMethods"]
methods.should.have.key("Quantity").equals(2)
methods.should.have.key("Items")
set(methods["Items"]).should.equal({"HEAD", "GET"})
methods.should.have.key("CachedMethods")
cached_methods = methods["CachedMethods"]
cached_methods.should.have.key("Quantity").equals(2)
set(cached_methods["Items"]).should.equal({"HEAD", "GET"})
default_cache.should.have.key("SmoothStreaming").equals(False)
default_cache.should.have.key("Compress").equals(True)
default_cache.should.have.key("LambdaFunctionAssociations").equals({"Quantity": 0})
default_cache.should.have.key("FunctionAssociations").equals({"Quantity": 0})
default_cache.should.have.key("FieldLevelEncryptionId").equals("")
default_cache.should.have.key("CachePolicyId")
config.should.have.key("CacheBehaviors").equals({"Quantity": 0})
config.should.have.key("CustomErrorResponses").equals({"Quantity": 0})
config.should.have.key("Comment").equals("")
config.should.have.key("Logging")
logging = config["Logging"]
logging.should.have.key("Enabled").equals(False)
logging.should.have.key("IncludeCookies").equals(False)
logging.should.have.key("Bucket").equals("")
logging.should.have.key("Prefix").equals("")
config.should.have.key("PriceClass").equals("PriceClass_All")
config.should.have.key("Enabled").equals(False)
config.should.have.key("WebACLId")
config.should.have.key("HttpVersion").equals("http2")
config.should.have.key("IsIPV6Enabled").equals(True)
config.should.have.key("ViewerCertificate")
cert = config["ViewerCertificate"]
cert.should.have.key("CloudFrontDefaultCertificate").equals(True)
cert.should.have.key("MinimumProtocolVersion").equals("TLSv1")
cert.should.have.key("CertificateSource").equals("cloudfront")
config.should.have.key("Restrictions")
config["Restrictions"].should.have.key("GeoRestriction")
restriction = config["Restrictions"]["GeoRestriction"]
restriction.should.have.key("RestrictionType").equals("none")
restriction.should.have.key("Quantity").equals(0)
@mock_cloudfront
def test_create_distribution_returns_etag():
client = boto3.client("cloudfront", region_name="us-east-1")
config = example_distribution_config("ref")
resp = client.create_distribution(DistributionConfig=config)
dist_id = resp["Distribution"]["Id"]
headers = resp["ResponseMetadata"]["HTTPHeaders"]
headers.should.have.key("etag").length_of(13)
headers.should.have.key("location").equals(
f"https://cloudfront.amazonaws.com/2020-05-31/distribution/{dist_id}"
)
@mock_cloudfront
def test_create_distribution_needs_unique_caller_reference():
client = boto3.client("cloudfront", region_name="us-east-1")
# Create standard distribution
config = example_distribution_config(ref="ref")
dist1 = client.create_distribution(DistributionConfig=config)
dist1_id = dist1["Distribution"]["Id"]
# Try to create distribution with the same ref
with pytest.raises(ClientError) as exc:
client.create_distribution(DistributionConfig=config)
err = exc.value.response["Error"]
err["Code"].should.equal("DistributionAlreadyExists")
err["Message"].should.equal(
f"The caller reference that you are using to create a distribution is associated with another distribution. Already exists: {dist1_id}"
)
# Creating another distribution with a different reference
config = example_distribution_config(ref="ref2")
dist2 = client.create_distribution(DistributionConfig=config)
dist1_id.shouldnt.equal(dist2["Distribution"]["Id"])
# TODO: Verify two exist, using the list_distributions method
@mock_cloudfront
def test_create_distribution_with_mismatched_originid():
client = boto3.client("cloudfront", region_name="us-west-1")
with pytest.raises(ClientError) as exc:
client.create_distribution(
DistributionConfig={
"CallerReference": "ref",
"Origins": {
"Quantity": 1,
"Items": [{"Id": "origin1", "DomainName": "https://getmoto.org",}],
},
"DefaultCacheBehavior": {
"TargetOriginId": "asdf",
"ViewerProtocolPolicy": "allow-all",
},
"Comment": "an optional comment that's not actually optional",
"Enabled": False,
}
)
metadata = exc.value.response["ResponseMetadata"]
metadata["HTTPStatusCode"].should.equal(404)
err = exc.value.response["Error"]
err["Code"].should.equal("NoSuchOrigin")
err["Message"].should.equal(
"One or more of your origins or origin groups do not exist."
)
@mock_cloudfront
def test_create_origin_without_origin_config():
client = boto3.client("cloudfront", region_name="us-west-1")
with pytest.raises(ClientError) as exc:
client.create_distribution(
DistributionConfig={
"CallerReference": "ref",
"Origins": {
"Quantity": 1,
"Items": [{"Id": "origin1", "DomainName": "https://getmoto.org",}],
},
"DefaultCacheBehavior": {
"TargetOriginId": "origin1",
"ViewerProtocolPolicy": "allow-all",
},
"Comment": "an optional comment that's not actually optional",
"Enabled": False,
}
)
metadata = exc.value.response["ResponseMetadata"]
metadata["HTTPStatusCode"].should.equal(400)
err = exc.value.response["Error"]
err["Code"].should.equal("InvalidOrigin")
err["Message"].should.equal(
"The specified origin server does not exist or is not valid."
)
@mock_cloudfront
def test_create_distribution_with_invalid_s3_bucket():
client = boto3.client("cloudfront", region_name="us-west-1")
with pytest.raises(ClientError) as exc:
client.create_distribution(
DistributionConfig={
"CallerReference": "ref",
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "origin1",
"DomainName": "https://getmoto.org",
"S3OriginConfig": {"OriginAccessIdentity": ""},
}
],
},
"DefaultCacheBehavior": {
"TargetOriginId": "origin1",
"ViewerProtocolPolicy": "allow-all",
},
"Comment": "an optional comment that's not actually optional",
"Enabled": False,
}
)
metadata = exc.value.response["ResponseMetadata"]
metadata["HTTPStatusCode"].should.equal(400)
err = exc.value.response["Error"]
err["Code"].should.equal("InvalidArgument")
err["Message"].should.equal(
"The parameter Origin DomainName does not refer to a valid S3 bucket."
)
@mock_cloudfront
def test_list_distributions_without_any():
client = boto3.client("cloudfront", region_name="us-east-1")
resp = client.list_distributions()
resp.should.have.key("DistributionList")
dlist = resp["DistributionList"]
dlist.should.have.key("Marker").equals("")
dlist.should.have.key("MaxItems").equals(100)
dlist.should.have.key("IsTruncated").equals(False)
dlist.should.have.key("Quantity").equals(0)
dlist.shouldnt.have.key("Items")
@mock_cloudfront
def test_list_distributions():
client = boto3.client("cloudfront", region_name="us-east-1")
config = example_distribution_config(ref="ref1")
dist1 = client.create_distribution(DistributionConfig=config)["Distribution"]
config = example_distribution_config(ref="ref2")
dist2 = client.create_distribution(DistributionConfig=config)["Distribution"]
resp = client.list_distributions()
resp.should.have.key("DistributionList")
dlist = resp["DistributionList"]
dlist.should.have.key("Quantity").equals(2)
dlist.should.have.key("Items").length_of(2)
item1 = dlist["Items"][0]
item1.should.have.key("Id").equals(dist1["Id"])
item1.should.have.key("ARN")
item1.should.have.key("Status").equals("Deployed")
item2 = dlist["Items"][1]
item2.should.have.key("Id").equals(dist2["Id"])
item2.should.have.key("ARN")
item2.should.have.key("Status").equals("Deployed")
@mock_cloudfront
def test_get_distribution():
client = boto3.client("cloudfront", region_name="us-east-1")
# Create standard distribution
config = example_distribution_config(ref="ref")
dist = client.create_distribution(DistributionConfig=config)
dist_id = dist["Distribution"]["Id"]
resp = client.get_distribution(Id=dist_id)
headers = resp["ResponseMetadata"]["HTTPHeaders"]
headers.should.have.key("etag").length_of(13)
dist = resp["Distribution"]
dist.should.have.key("Id").equals(dist_id)
dist.should.have.key("Status").equals("Deployed")
dist.should.have.key("DomainName").equals(dist["DomainName"])
dist.should.have.key("DistributionConfig")
config = dist["DistributionConfig"]
config.should.have.key("CallerReference").should.equal("ref")
@mock_cloudfront
def test_get_unknown_distribution():
client = boto3.client("cloudfront", region_name="us-west-1")
with pytest.raises(ClientError) as exc:
# Should have a second param, IfMatch, that contains the ETag of the most recent GetDistribution-request
client.get_distribution(Id="unknown")
metadata = exc.value.response["ResponseMetadata"]
metadata["HTTPStatusCode"].should.equal(404)
err = exc.value.response["Error"]
err["Code"].should.equal("NoSuchDistribution")
err["Message"].should.equal("The specified distribution does not exist.")
@mock_cloudfront
def test_delete_unknown_distribution():
client = boto3.client("cloudfront", region_name="us-west-1")
with pytest.raises(ClientError) as exc:
client.delete_distribution(Id="unknown", IfMatch="..")
metadata = exc.value.response["ResponseMetadata"]
metadata["HTTPStatusCode"].should.equal(404)
err = exc.value.response["Error"]
err["Code"].should.equal("NoSuchDistribution")
err["Message"].should.equal("The specified distribution does not exist.")
@mock_cloudfront
def test_delete_distribution_without_ifmatch():
client = boto3.client("cloudfront", region_name="us-west-1")
with pytest.raises(ClientError) as exc:
# Should have a second param, IfMatch, that contains the ETag of the most recent GetDistribution-request
client.delete_distribution(Id="...")
metadata = exc.value.response["ResponseMetadata"]
metadata["HTTPStatusCode"].should.equal(400)
err = exc.value.response["Error"]
err["Code"].should.equal("InvalidIfMatchVersion")
err["Message"].should.equal(
"The If-Match version is missing or not valid for the resource."
)
@mock_cloudfront
def test_delete_distribution_random_etag():
"""
Etag validation is not implemented yet
Calling the delete-method with any etag will pass
"""
client = boto3.client("cloudfront", region_name="us-east-1")
# Create standard distribution
config = example_distribution_config(ref="ref")
dist1 = client.create_distribution(DistributionConfig=config)
dist_id = dist1["Distribution"]["Id"]
client.delete_distribution(Id=dist_id, IfMatch="anything")
with pytest.raises(ClientError) as exc:
client.get_distribution(Id=dist_id)
err = exc.value.response["Error"]
err["Code"].should.equal("NoSuchDistribution")

View File

@ -0,0 +1,14 @@
import sure # noqa # pylint: disable=unused-import
import xmltodict
import moto.server as server
def test_cloudfront_list():
backend = server.create_backend_app("cloudfront")
test_client = backend.test_client()
res = test_client.get("/2020-05-31/distribution")
data = xmltodict.parse(res.data, dict_constructor=dict)
data.should.have.key("DistributionList")
data["DistributionList"].shouldnt.have.key("Items")