Feature - CloudFront (#4640)
This commit is contained in:
		
							parent
							
								
									41de9b82ac
								
							
						
					
					
						commit
						2cf37a4b90
					
				| @ -398,6 +398,104 @@ | |||||||
| - [X] validate_template | - [X] validate_template | ||||||
| </details> | </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 | ## cloudtrail | ||||||
| <details> | <details> | ||||||
| <summary>44% implemented</summary> | <summary>44% implemented</summary> | ||||||
| @ -4711,7 +4809,6 @@ | |||||||
| - cloud9 | - cloud9 | ||||||
| - cloudcontrol | - cloudcontrol | ||||||
| - clouddirectory | - clouddirectory | ||||||
| - cloudfront |  | ||||||
| - cloudhsm | - cloudhsm | ||||||
| - cloudhsmv2 | - cloudhsmv2 | ||||||
| - cloudsearch | - cloudsearch | ||||||
|  | |||||||
							
								
								
									
										134
									
								
								docs/docs/services/cloudfront.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								docs/docs/services/cloudfront.rst
									
									
									
									
									
										Normal 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 | ||||||
|  | 
 | ||||||
| @ -43,6 +43,7 @@ mock_cloudformation = lazy_load(".cloudformation", "mock_cloudformation") | |||||||
| mock_cloudformation_deprecated = lazy_load( | mock_cloudformation_deprecated = lazy_load( | ||||||
|     ".cloudformation", "mock_cloudformation_deprecated" |     ".cloudformation", "mock_cloudformation_deprecated" | ||||||
| ) | ) | ||||||
|  | mock_cloudfront = lazy_load(".cloudfront", "mock_cloudfront") | ||||||
| mock_cloudtrail = lazy_load(".cloudtrail", "mock_cloudtrail", boto3_name="cloudtrail") | mock_cloudtrail = lazy_load(".cloudtrail", "mock_cloudtrail", boto3_name="cloudtrail") | ||||||
| mock_cloudwatch = lazy_load(".cloudwatch", "mock_cloudwatch") | mock_cloudwatch = lazy_load(".cloudwatch", "mock_cloudwatch") | ||||||
| mock_cloudwatch_deprecated = lazy_load(".cloudwatch", "mock_cloudwatch_deprecated") | mock_cloudwatch_deprecated = lazy_load(".cloudwatch", "mock_cloudwatch_deprecated") | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ backend_url_patterns = [ | |||||||
|     ("batch", re.compile("https?://batch\\.(.+)\\.amazonaws.com")), |     ("batch", re.compile("https?://batch\\.(.+)\\.amazonaws.com")), | ||||||
|     ("budgets", re.compile("https?://budgets\\.amazonaws\\.com")), |     ("budgets", re.compile("https?://budgets\\.amazonaws\\.com")), | ||||||
|     ("cloudformation", re.compile("https?://cloudformation\\.(.+)\\.amazonaws\\.com")), |     ("cloudformation", re.compile("https?://cloudformation\\.(.+)\\.amazonaws\\.com")), | ||||||
|  |     ("cloudfront", re.compile("https?://cloudfront\\.amazonaws\\.com")), | ||||||
|     ("cloudtrail", re.compile("https?://cloudtrail\\.(.+)\\.amazonaws\\.com")), |     ("cloudtrail", re.compile("https?://cloudtrail\\.(.+)\\.amazonaws\\.com")), | ||||||
|     ("cloudwatch", re.compile("https?://monitoring\\.(.+)\\.amazonaws.com")), |     ("cloudwatch", re.compile("https?://monitoring\\.(.+)\\.amazonaws.com")), | ||||||
|     ("codecommit", re.compile("https?://codecommit\\.(.+)\\.amazonaws\\.com")), |     ("codecommit", re.compile("https?://codecommit\\.(.+)\\.amazonaws\\.com")), | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								moto/cloudfront/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								moto/cloudfront/__init__.py
									
									
									
									
									
										Normal 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) | ||||||
							
								
								
									
										81
									
								
								moto/cloudfront/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								moto/cloudfront/exceptions.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										224
									
								
								moto/cloudfront/models.py
									
									
									
									
									
										Normal 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() | ||||||
							
								
								
									
										514
									
								
								moto/cloudfront/responses.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								moto/cloudfront/responses.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										13
									
								
								moto/cloudfront/urls.py
									
									
									
									
									
										Normal 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, | ||||||
|  | } | ||||||
| @ -144,7 +144,7 @@ class convert_flask_to_httpretty_response(object): | |||||||
|         from flask import request, Response |         from flask import request, Response | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             result = self.callback(request, request.url, {}) |             result = self.callback(request, request.url, dict(request.headers)) | ||||||
|         except ClientError as exc: |         except ClientError as exc: | ||||||
|             result = 400, {}, exc.response["Error"]["Message"] |             result = 400, {}, exc.response["Error"]["Message"] | ||||||
|         # result is a status, headers, response tuple |         # result is a status, headers, response tuple | ||||||
|  | |||||||
| @ -119,8 +119,8 @@ class DomainDispatcherApplication(object): | |||||||
|                 # S3 is the last resort when the target is also unknown |                 # S3 is the last resort when the target is also unknown | ||||||
|                 service, region = DEFAULT_SERVICE_REGION |                 service, region = DEFAULT_SERVICE_REGION | ||||||
| 
 | 
 | ||||||
|         if service == "budgets": |         if service in ["budgets", "cloudfront"]: | ||||||
|             # Budgets is global |             # Global Services - they do not have/expect a region | ||||||
|             host = f"{service}.amazonaws.com" |             host = f"{service}.amazonaws.com" | ||||||
|         elif service == "mediastore" and not target: |         elif service == "mediastore" and not target: | ||||||
|             # All MediaStore API calls have a target header |             # All MediaStore API calls have a target header | ||||||
|  | |||||||
							
								
								
									
										0
									
								
								tests/test_cloudfront/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/test_cloudfront/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										415
									
								
								tests/test_cloudfront/test_cloudfront_distributions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										415
									
								
								tests/test_cloudfront/test_cloudfront_distributions.py
									
									
									
									
									
										Normal 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") | ||||||
							
								
								
									
										14
									
								
								tests/test_cloudfront/test_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								tests/test_cloudfront/test_server.py
									
									
									
									
									
										Normal 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") | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user