Repository: getmoto/moto
Pull request #6430 (2023-06-21 17:11:37)

add support for ssm maintenance window tasks
This PR adds support for some operations related to Maintenance Window Tasks:

- [X] register_task_with_maintenance_window
- [X] describe_maintenance_window_tasks
- [X] deregister_task_from_maintenance_window

I'll rebase it after #6429 is merged

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/6430?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#6430](https://app.codecov.io/gh/getmoto/moto/pull/6430?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (69dfcd4) into [master](https://app.codecov.io/gh/getmoto/moto/commit/af4a8e2ba6faf0c95d836f8cb66d73d80bd6250b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (af4a8e2) will **increase** coverage by `0.00%`.
> The diff coverage is `97.95%`.

```diff
@@           Coverage Diff           @@
##           master    #6430   +/-   ##
=======================================
  Coverage   96.16%   96.17%           
=======================================
  Files         800      800           
  Lines       78160    78258   +98     
=======================================
+ Hits        75165    75261   +96     
- Misses       2995     2997    +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| servertests | `36.88% <19.38%> (-0.03%)` | :arrow_down: |
| unittests | `96.11% <97.95%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://app.codecov.io/gh/getmoto/moto/pull/6430?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [moto/ssm/models.py](https://app.codecov.io/gh/getmoto/moto/pull/6430?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9zc20vbW9kZWxzLnB5) | `95.18% <97.22%> (+0.14%)` | :arrow_up: |
| [moto/ssm/responses.py](https://app.codecov.io/gh/getmoto/moto/pull/6430?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW90by9zc20vcmVzcG9uc2VzLnB5) | `100.00% <100.00%> (ø)` | |


mikegrima: Codecov complains that there are 2 spots not covered by tests. Mostly a nitpick, but we would prefer you have coverage for it.

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index faf335e50..3a8ac6584 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -738,7 +738,7 @@
 
 ## cloudfront
 <details>
-<summary>9% implemented</summary>
+<summary>14% implemented</summary>
 
 - [ ] associate_alias
 - [ ] copy_distribution
@@ -753,7 +753,7 @@
 - [X] create_invalidation
 - [ ] create_key_group
 - [ ] create_monitoring_subscription
-- [ ] create_origin_access_control
+- [X] create_origin_access_control
 - [ ] create_origin_request_policy
 - [ ] create_public_key
 - [ ] create_realtime_log_config
@@ -769,7 +769,7 @@
 - [ ] delete_function
 - [ ] delete_key_group
 - [ ] delete_monitoring_subscription
-- [ ] delete_origin_access_control
+- [X] delete_origin_access_control
 - [ ] delete_origin_request_policy
 - [ ] delete_public_key
 - [ ] delete_realtime_log_config
@@ -793,7 +793,7 @@
 - [ ] get_key_group
 - [ ] get_key_group_config
 - [ ] get_monitoring_subscription
-- [ ] get_origin_access_control
+- [X] get_origin_access_control
 - [ ] get_origin_access_control_config
 - [ ] get_origin_request_policy
 - [ ] get_origin_request_policy_config
@@ -820,7 +820,7 @@
 - [ ] list_functions
 - [X] list_invalidations
 - [ ] list_key_groups
-- [ ] list_origin_access_controls
+- [X] list_origin_access_controls
 - [ ] list_origin_request_policies
 - [ ] list_public_keys
 - [ ] list_realtime_log_configs
@@ -840,7 +840,7 @@
 - [ ] update_field_level_encryption_profile
 - [ ] update_function
 - [ ] update_key_group
-- [ ] update_origin_access_control
+- [X] update_origin_access_control
 - [ ] update_origin_request_policy
 - [ ] update_public_key
 - [ ] update_realtime_log_config
@@ -6784,7 +6784,7 @@
 - [ ] deregister_managed_instance
 - [ ] deregister_patch_baseline_for_patch_group
 - [X] deregister_target_from_maintenance_window
-- [ ] deregister_task_from_maintenance_window
+- [X] deregister_task_from_maintenance_window
 - [ ] describe_activations
 - [ ] describe_association
 - [ ] describe_association_execution_targets
@@ -6807,7 +6807,7 @@
 - [ ] describe_maintenance_window_executions
 - [ ] describe_maintenance_window_schedule
 - [X] describe_maintenance_window_targets
-- [ ] describe_maintenance_window_tasks
+- [X] describe_maintenance_window_tasks
 - [X] describe_maintenance_windows
 - [ ] describe_maintenance_windows_for_target
 - [ ] describe_ops_items
@@ -6868,7 +6868,7 @@
 - [ ] register_default_patch_baseline
 - [ ] register_patch_baseline_for_patch_group
 - [X] register_target_with_maintenance_window
-- [ ] register_task_with_maintenance_window
+- [X] register_task_with_maintenance_window
 - [X] remove_tags_from_resource
 - [ ] reset_service_setting
 - [ ] resume_session
diff --git a/docs/docs/configuration/environment_variables.rst b/docs/docs/configuration/environment_variables.rst
index 657031637..a717abe92 100644
--- a/docs/docs/configuration/environment_variables.rst
+++ b/docs/docs/configuration/environment_variables.rst
@@ -24,6 +24,8 @@ The following is a non-exhaustive list of the environment variables that can be
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 | MOTO_ALLOW_NONEXISTENT_REGION | bool     | False     |                                                                                                 |
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
+| MOTO_ENABLE_ISO_REGIONS       | bool     | False     | Enable the mocking of ISO regions (ISO, ISO-B, ISO-E, ISO-F)                                    |
++-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 |                               |          |           |                                                                                                 |
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 | MOTO_S3_CUSTOM_ENDPOINTS      | str      |           | See :ref:`s3`.                                                                                  |
diff --git a/docs/docs/services/cloudfront.rst b/docs/docs/services/cloudfront.rst
index 655a26573..0b84b7555 100644
--- a/docs/docs/services/cloudfront.rst
+++ b/docs/docs/services/cloudfront.rst
@@ -44,7 +44,7 @@ cloudfront
 - [X] create_invalidation
 - [ ] create_key_group
 - [ ] create_monitoring_subscription
-- [ ] create_origin_access_control
+- [X] create_origin_access_control
 - [ ] create_origin_request_policy
 - [ ] create_public_key
 - [ ] create_realtime_log_config
@@ -65,7 +65,11 @@ cloudfront
 - [ ] delete_function
 - [ ] delete_key_group
 - [ ] delete_monitoring_subscription
-- [ ] delete_origin_access_control
+- [X] delete_origin_access_control
+  
+        The IfMatch-parameter is not yet implemented
+        
+
 - [ ] delete_origin_request_policy
 - [ ] delete_public_key
 - [ ] delete_realtime_log_config
@@ -89,7 +93,7 @@ cloudfront
 - [ ] get_key_group
 - [ ] get_key_group_config
 - [ ] get_monitoring_subscription
-- [ ] get_origin_access_control
+- [X] get_origin_access_control
 - [ ] get_origin_access_control_config
 - [ ] get_origin_request_policy
 - [ ] get_origin_request_policy_config
@@ -124,7 +128,11 @@ cloudfront
         
 
 - [ ] list_key_groups
-- [ ] list_origin_access_controls
+- [X] list_origin_access_controls
+  
+        Pagination is not yet implemented
+        
+
 - [ ] list_origin_request_policies
 - [ ] list_public_keys
 - [ ] list_realtime_log_configs
@@ -149,7 +157,11 @@ cloudfront
 - [ ] update_field_level_encryption_profile
 - [ ] update_function
 - [ ] update_key_group
-- [ ] update_origin_access_control
+- [X] update_origin_access_control
+  
+        The IfMatch-parameter is not yet implemented
+        
+
 - [ ] update_origin_request_policy
 - [ ] update_public_key
 - [ ] update_realtime_log_config
diff --git a/moto/cloudfront/exceptions.py b/moto/cloudfront/exceptions.py
index 268faed53..48dc3a5dd 100644
--- a/moto/cloudfront/exceptions.py
+++ b/moto/cloudfront/exceptions.py
@@ -13,7 +13,6 @@ EXCEPTION_RESPONSE = """<?xml version="1.0"?>
 
 
 class CloudFrontException(RESTError):
-
     code = 400
 
     def __init__(self, error_type: str, message: str, **kwargs: Any):
@@ -23,7 +22,6 @@ class CloudFrontException(RESTError):
 
 
 class OriginDoesNotExist(CloudFrontException):
-
     code = 404
 
     def __init__(self) -> None:
@@ -66,10 +64,19 @@ class InvalidIfMatchVersion(CloudFrontException):
 
 
 class NoSuchDistribution(CloudFrontException):
-
     code = 404
 
     def __init__(self) -> None:
         super().__init__(
             "NoSuchDistribution", message="The specified distribution does not exist."
         )
+
+
+class NoSuchOriginAccessControl(CloudFrontException):
+    code = 404
+
+    def __init__(self) -> None:
+        super().__init__(
+            "NoSuchOriginAccessControl",
+            message="The specified origin access control does not exist.",
+        )
diff --git a/moto/cloudfront/models.py b/moto/cloudfront/models.py
index 52903a13d..5f54f0cb9 100644
--- a/moto/cloudfront/models.py
+++ b/moto/cloudfront/models.py
@@ -16,6 +16,7 @@ from .exceptions import (
     DistributionAlreadyExists,
     InvalidIfMatchVersion,
     NoSuchDistribution,
+    NoSuchOriginAccessControl,
 )
 
 
@@ -212,6 +213,29 @@ class Distribution(BaseModel, ManagedState):
         return f"https://cloudfront.amazonaws.com/2020-05-31/distribution/{self.distribution_id}"
 
 
+class OriginAccessControl(BaseModel):
+    def __init__(self, config_dict: Dict[str, str]):
+        self.id = Invalidation.random_id()
+        self.name = config_dict.get("Name")
+        self.description = config_dict.get("Description")
+        self.signing_protocol = config_dict.get("SigningProtocol")
+        self.signing_behaviour = config_dict.get("SigningBehavior")
+        self.origin_type = config_dict.get("OriginAccessControlOriginType")
+        self.etag = Invalidation.random_id()
+
+    def update(self, config: Dict[str, str]) -> None:
+        if "Name" in config:
+            self.name = config["Name"]
+        if "Description" in config:
+            self.description = config["Description"]
+        if "SigningProtocol" in config:
+            self.signing_protocol = config["SigningProtocol"]
+        if "SigningBehavior" in config:
+            self.signing_behaviour = config["SigningBehavior"]
+        if "OriginAccessControlOriginType" in config:
+            self.origin_type = config["OriginAccessControlOriginType"]
+
+
 class Invalidation(BaseModel):
     @staticmethod
     def random_id(uppercase: bool = True) -> str:
@@ -243,6 +267,7 @@ class CloudFrontBackend(BaseBackend):
         super().__init__(region_name, account_id)
         self.distributions: Dict[str, Distribution] = dict()
         self.invalidations: Dict[str, List[Invalidation]] = dict()
+        self.origin_access_controls: Dict[str, OriginAccessControl] = dict()
         self.tagger = TaggingService()
 
         state_manager.register_default_transition(
@@ -363,6 +388,40 @@ class CloudFrontBackend(BaseBackend):
     def list_tags_for_resource(self, resource: str) -> Dict[str, List[Dict[str, str]]]:
         return self.tagger.list_tags_for_resource(resource)
 
+    def create_origin_access_control(
+        self, config_dict: Dict[str, str]
+    ) -> OriginAccessControl:
+        control = OriginAccessControl(config_dict)
+        self.origin_access_controls[control.id] = control
+        return control
+
+    def get_origin_access_control(self, control_id: str) -> OriginAccessControl:
+        if control_id not in self.origin_access_controls:
+            raise NoSuchOriginAccessControl
+        return self.origin_access_controls[control_id]
+
+    def update_origin_access_control(
+        self, control_id: str, config: Dict[str, str]
+    ) -> OriginAccessControl:
+        """
+        The IfMatch-parameter is not yet implemented
+        """
+        control = self.get_origin_access_control(control_id)
+        control.update(config)
+        return control
+
+    def list_origin_access_controls(self) -> Iterable[OriginAccessControl]:
+        """
+        Pagination is not yet implemented
+        """
+        return self.origin_access_controls.values()
+
+    def delete_origin_access_control(self, control_id: str) -> None:
+        """
+        The IfMatch-parameter is not yet implemented
+        """
+        self.origin_access_controls.pop(control_id)
+
 
 cloudfront_backends = BackendDict(
     CloudFrontBackend,
diff --git a/moto/cloudfront/responses.py b/moto/cloudfront/responses.py
index e4499953e..8ec785ca8 100644
--- a/moto/cloudfront/responses.py
+++ b/moto/cloudfront/responses.py
@@ -39,6 +39,22 @@ class CloudFrontResponse(BaseResponse):
         if request.method == "GET":
             return self.list_tags_for_resource()
 
+    def origin_access_controls(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
+        self.setup_class(request, full_url, headers)
+        if request.method == "POST":
+            return self.create_origin_access_control()
+        if request.method == "GET":
+            return self.list_origin_access_controls()
+
+    def origin_access_control(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
+        self.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return self.get_origin_access_control()
+        if request.method == "PUT":
+            return self.update_origin_access_control()
+        if request.method == "DELETE":
+            return self.delete_origin_access_control()
+
     def create_distribution(self) -> TYPE_RESPONSE:
         params = self._get_xml_body()
         if "DistributionConfigWithTags" in params:
@@ -130,6 +146,37 @@ class CloudFrontResponse(BaseResponse):
         response = template.render(tags=tags, xmlns=XMLNS)
         return 200, {}, response
 
+    def create_origin_access_control(self) -> TYPE_RESPONSE:
+        config = self._get_xml_body().get("OriginAccessControlConfig", {})
+        config.pop("@xmlns", None)
+        control = self.backend.create_origin_access_control(config)
+        template = self.response_template(ORIGIN_ACCESS_CONTROl)
+        return 200, {}, template.render(control=control)
+
+    def get_origin_access_control(self) -> TYPE_RESPONSE:
+        control_id = self.path.split("/")[-1]
+        control = self.backend.get_origin_access_control(control_id)
+        template = self.response_template(ORIGIN_ACCESS_CONTROl)
+        return 200, {"ETag": control.etag}, template.render(control=control)
+
+    def list_origin_access_controls(self) -> TYPE_RESPONSE:
+        controls = self.backend.list_origin_access_controls()
+        template = self.response_template(LIST_ORIGIN_ACCESS_CONTROl)
+        return 200, {}, template.render(controls=controls)
+
+    def update_origin_access_control(self) -> TYPE_RESPONSE:
+        control_id = self.path.split("/")[-2]
+        config = self._get_xml_body().get("OriginAccessControlConfig", {})
+        config.pop("@xmlns", None)
+        control = self.backend.update_origin_access_control(control_id, config)
+        template = self.response_template(ORIGIN_ACCESS_CONTROl)
+        return 200, {"ETag": control.etag}, template.render(control=control)
+
+    def delete_origin_access_control(self) -> TYPE_RESPONSE:
+        control_id = self.path.split("/")[-1]
+        self.backend.delete_origin_access_control(control_id)
+        return 200, {}, "{}"
+
 
 DIST_META_TEMPLATE = """
     <Id>{{ distribution.distribution_id }}</Id>
@@ -651,3 +698,39 @@ TAGS_TEMPLATE = """<?xml version="1.0"?>
   </Items>
 </Tags>
 """
+
+
+ORIGIN_ACCESS_CONTROl = """<?xml version="1.0"?>
+<OriginAccessControl>
+  <Id>{{ control.id }}</Id>
+  <OriginAccessControlConfig>
+    <Name>{{ control.name }}</Name>
+    {% if control.description %}
+    <Description>{{ control.description }}</Description>
+    {% endif %}
+    <SigningProtocol>{{ control.signing_protocol }}</SigningProtocol>
+    <SigningBehavior>{{ control.signing_behaviour }}</SigningBehavior>
+    <OriginAccessControlOriginType>{{ control.origin_type }}</OriginAccessControlOriginType>
+  </OriginAccessControlConfig>
+</OriginAccessControl>
+"""
+
+
+LIST_ORIGIN_ACCESS_CONTROl = """<?xml version="1.0"?>
+<OriginAccessControlList>
+  <Items>
+  {% for control in controls %}
+    <OriginAccessControlSummary>
+      <Id>{{ control.id }}</Id>
+      <Name>{{ control.name }}</Name>
+      {% if control.description %}
+      <Description>{{ control.description }}</Description>
+      {% endif %}
+      <SigningProtocol>{{ control.signing_protocol }}</SigningProtocol>
+      <SigningBehavior>{{ control.signing_behaviour }}</SigningBehavior>
+      <OriginAccessControlOriginType>{{ control.origin_type }}</OriginAccessControlOriginType>
+    </OriginAccessControlSummary>
+  {% endfor %}
+  </Items>
+</OriginAccessControlList>
+"""
diff --git a/moto/cloudfront/urls.py b/moto/cloudfront/urls.py
index a65a5776c..89f93a866 100644
--- a/moto/cloudfront/urls.py
+++ b/moto/cloudfront/urls.py
@@ -13,4 +13,7 @@ url_paths = {
     "{0}/2020-05-31/distribution/(?P<distribution_id>[^/]+)/config$": response.update_distribution,
     "{0}/2020-05-31/distribution/(?P<distribution_id>[^/]+)/invalidation": response.invalidation,
     "{0}/2020-05-31/tagging$": response.tags,
+    "{0}/2020-05-31/origin-access-control$": response.origin_access_controls,
+    "{0}/2020-05-31/origin-access-control/(?P<oac_id>[^/]+)$": response.origin_access_control,
+    "{0}/2020-05-31/origin-access-control/(?P<oac_id>[^/]+)/config$": response.origin_access_control,
 }
diff --git a/moto/core/base_backend.py b/moto/core/base_backend.py
index b5ce8a377..10ea6a225 100644
--- a/moto/core/base_backend.py
+++ b/moto/core/base_backend.py
@@ -5,7 +5,7 @@ from functools import lru_cache
 from threading import RLock
 from typing import Any, List, Dict, Optional, ClassVar, TypeVar, Iterator
 from uuid import uuid4
-from moto.settings import allow_unknown_region
+from moto.settings import allow_unknown_region, enable_iso_regions
 from .model_instances import model_data
 from .utils import convert_regex_to_flask_path
 
@@ -56,13 +56,16 @@ class BaseBackend:
         for url_base in url_bases:
             # The default URL_base will look like: http://service.[..].amazonaws.com/...
             # This extension ensures support for the China & ISO regions
-            alt_dns_suffixes = {
-                "cn": "amazonaws.com.cn",
-                "iso": "c2s.ic.gov",
-                "isob": "sc2s.sgov.gov",
-                "isoe": "cloud.adc-e.uk",
-                "isof": "csp.hci.ic.gov",
-            }
+            alt_dns_suffixes = {"cn": "amazonaws.com.cn"}
+            if enable_iso_regions():
+                alt_dns_suffixes.update(
+                    {
+                        "iso": "c2s.ic.gov",
+                        "isob": "sc2s.sgov.gov",
+                        "isoe": "cloud.adc-e.uk",
+                        "isof": "csp.hci.ic.gov",
+                    }
+                )
 
             for url_path, handler in unformatted_paths.items():
                 url = url_path.format(url_base)
diff --git a/moto/core/custom_responses_mock.py b/moto/core/custom_responses_mock.py
index d7c5107ee..b9ff346f1 100644
--- a/moto/core/custom_responses_mock.py
+++ b/moto/core/custom_responses_mock.py
@@ -23,6 +23,11 @@ class CallbackResponse(responses.CallbackResponse):
     Need to subclass so we can change a couple things
     """
 
+    def __eq__(self, other: Any) -> bool:
+        if isinstance(other, CallbackResponse):
+            return self.method == other.method and self.url.pattern == other.url.pattern  # type: ignore
+        return super().__eq__(other)
+
     def get_response(self, request: Any) -> responses.HTTPResponse:
         """
         Need to override this so we can pass decode_content=False
diff --git a/moto/core/responses_custom_registry.py b/moto/core/responses_custom_registry.py
index 30f800b51..f21a8bda4 100644
--- a/moto/core/responses_custom_registry.py
+++ b/moto/core/responses_custom_registry.py
@@ -1,6 +1,7 @@
 # This will only exist in responses >= 0.17
 import responses
-from typing import Any, List, Tuple, Optional
+from collections import defaultdict
+from typing import Any, Dict, List, Tuple, Optional
 from .custom_responses_mock import CallbackResponse, not_implemented_callback
 
 
@@ -9,15 +10,25 @@ class CustomRegistry(responses.registries.FirstMatchRegistry):
     Custom Registry that returns requests in an order that makes sense for Moto:
      - Implemented callbacks take precedence over non-implemented-callbacks
      - CallbackResponses are not discarded after first use - users can mock the same URL as often as they like
+     - CallbackResponses are persisted in a dictionary, with the request-method as key
+       This reduces the number of possible responses that we need to search
     """
 
+    def __init__(self) -> None:
+        self._registered: Dict[str, List[responses.BaseResponse]] = defaultdict(list)
+
     def add(self, response: responses.BaseResponse) -> responses.BaseResponse:
-        if response not in self.registered:
-            super().add(response)
+        if response not in self._registered[response.method]:
+            self._registered[response.method].append(response)
         return response
 
+    def reset(self) -> None:
+        self._registered.clear()
+
     def find(self, request: Any) -> Tuple[Optional[responses.BaseResponse], List[str]]:
-        all_possibles = responses._default_mock._registry.registered + self.registered
+        all_possibles = responses._default_mock._registry.registered
+        # We don't have to search through all possible methods - only the ones registered for this particular method
+        all_possibles.extend(self._registered[request.method])
         found = []
         match_failed_reasons = []
         for response in all_possibles:
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index bd805c0ea..1adc7430b 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -13,7 +13,6 @@ from moto.dynamodb.parsing.reserved_keywords import ReservedKeywords
 from .exceptions import (
     MockValidationException,
     ResourceNotFoundException,
-    ConditionalCheckFailed,
 )
 from moto.dynamodb.models import dynamodb_backends, Table, DynamoDBBackend
 from moto.dynamodb.models.utilities import dynamo_json_dump
@@ -806,12 +805,7 @@ class DynamoHandler(BaseResponse):
         if return_values not in ("ALL_OLD", "NONE"):
             raise MockValidationException("Return values set to invalid value")
 
-        try:
-            self.dynamodb_backend.get_table(name)
-        except ResourceNotFoundException:
-            raise ConditionalCheckFailed(
-                "A condition specified in the operation could not be evaluated."
-            )
+        self.dynamodb_backend.get_table(name)
 
         # Attempt to parse simple ConditionExpressions into an Expected
         # expression
diff --git a/moto/settings.py b/moto/settings.py
index 5ca0e6b13..2c7974879 100644
--- a/moto/settings.py
+++ b/moto/settings.py
@@ -145,3 +145,7 @@ def get_docker_host() -> str:
 
 def get_cognito_idp_user_pool_id_strategy() -> Optional[str]:
     return os.environ.get("MOTO_COGNITO_IDP_USER_POOL_ID_STRATEGY")
+
+
+def enable_iso_regions() -> bool:
+    return os.environ.get("MOTO_ENABLE_ISO_REGIONS", "false").lower() == "true"
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index f3c8734b0..d88deb808 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -975,6 +975,65 @@ class FakeMaintenanceWindowTarget:
         return str(random.uuid4())
 
 
+class FakeMaintenanceWindowTask:
+    def __init__(
+        self,
+        window_id: str,
+        targets: Optional[List[Dict[str, Any]]],
+        task_arn: str,
+        service_role_arn: Optional[str],
+        task_type: str,
+        task_parameters: Optional[Dict[str, Any]],
+        task_invocation_parameters: Optional[Dict[str, Any]],
+        priority: Optional[int],
+        max_concurrency: Optional[str],
+        max_errors: Optional[str],
+        logging_info: Optional[Dict[str, Any]],
+        name: Optional[str],
+        description: Optional[str],
+        cutoff_behavior: Optional[str],
+        alarm_configurations: Optional[Dict[str, Any]],
+    ):
+        self.window_task_id = FakeMaintenanceWindowTask.generate_id()
+        self.window_id = window_id
+        self.task_type = task_type
+        self.task_arn = task_arn
+        self.service_role_arn = service_role_arn
+        self.task_parameters = task_parameters
+        self.priority = priority
+        self.max_concurrency = max_concurrency
+        self.max_errors = max_errors
+        self.logging_info = logging_info
+        self.name = name
+        self.description = description
+        self.targets = targets
+        self.task_invocation_parameters = task_invocation_parameters
+        self.cutoff_behavior = cutoff_behavior
+        self.alarm_configurations = alarm_configurations
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "WindowId": self.window_id,
+            "WindowTaskId": self.window_task_id,
+            "TaskType": self.task_type,
+            "TaskArn": self.task_arn,
+            "ServiceRoleArn": self.service_role_arn,
+            "TaskParameters": self.task_parameters,
+            "Priority": self.priority,
+            "MaxConcurrency": self.max_concurrency,
+            "MaxErrors": self.max_errors,
+            "LoggingInfo": self.logging_info,
+            "Name": self.name,
+            "Description": self.description,
+            "Targets": self.targets,
+            "TaskInvocationParameters": self.task_invocation_parameters,
+        }
+
+    @staticmethod
+    def generate_id() -> str:
+        return str(random.uuid4())
+
+
 def _maintenance_window_target_filter_match(
     filters: Optional[List[Dict[str, Any]]], target: FakeMaintenanceWindowTarget
 ) -> bool:
@@ -983,6 +1042,14 @@ def _maintenance_window_target_filter_match(
     return False
 
 
+def _maintenance_window_task_filter_match(
+    filters: Optional[List[Dict[str, Any]]], task: FakeMaintenanceWindowTask
+) -> bool:
+    if not filters and task:
+        return True
+    return False
+
+
 class FakeMaintenanceWindow:
     def __init__(
         self,
@@ -1007,6 +1074,7 @@ class FakeMaintenanceWindow:
         self.start_date = start_date
         self.end_date = end_date
         self.targets: Dict[str, FakeMaintenanceWindowTarget] = {}
+        self.tasks: Dict[str, FakeMaintenanceWindowTask] = {}
 
     def to_json(self) -> Dict[str, Any]:
         return {
@@ -2351,5 +2419,62 @@ class SimpleSystemManagerBackend(BaseBackend):
         ]
         return targets
 
+    def register_task_with_maintenance_window(
+        self,
+        window_id: str,
+        targets: Optional[List[Dict[str, Any]]],
+        task_arn: str,
+        service_role_arn: Optional[str],
+        task_type: str,
+        task_parameters: Optional[Dict[str, Any]],
+        task_invocation_parameters: Optional[Dict[str, Any]],
+        priority: Optional[int],
+        max_concurrency: Optional[str],
+        max_errors: Optional[str],
+        logging_info: Optional[Dict[str, Any]],
+        name: Optional[str],
+        description: Optional[str],
+        cutoff_behavior: Optional[str],
+        alarm_configurations: Optional[Dict[str, Any]],
+    ) -> str:
+
+        window = self.get_maintenance_window(window_id)
+        task = FakeMaintenanceWindowTask(
+            window_id,
+            targets,
+            task_arn,
+            service_role_arn,
+            task_type,
+            task_parameters,
+            task_invocation_parameters,
+            priority,
+            max_concurrency,
+            max_errors,
+            logging_info,
+            name,
+            description,
+            cutoff_behavior,
+            alarm_configurations,
+        )
+        window.tasks[task.window_task_id] = task
+        return task.window_task_id
+
+    def describe_maintenance_window_tasks(
+        self, window_id: str, filters: List[Dict[str, Any]]
+    ) -> List[FakeMaintenanceWindowTask]:
+        window = self.get_maintenance_window(window_id)
+        tasks = [
+            task
+            for task in window.tasks.values()
+            if _maintenance_window_task_filter_match(filters, task)
+        ]
+        return tasks
+
+    def deregister_task_from_maintenance_window(
+        self, window_id: str, window_task_id: str
+    ) -> None:
+        window = self.get_maintenance_window(window_id)
+        del window.tasks[window_task_id]
+
 
 ssm_backends = BackendDict(SimpleSystemManagerBackend, "ssm")
diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py
index 3f757f03d..e488b6193 100644
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -513,3 +513,42 @@ class SimpleSystemManagerResponse(BaseResponse):
         baseline_id = self._get_param("BaselineId")
         self.ssm_backend.delete_patch_baseline(baseline_id)
         return "{}"
+
+    def register_task_with_maintenance_window(self) -> str:
+        window_task_id = self.ssm_backend.register_task_with_maintenance_window(
+            window_id=self._get_param("WindowId"),
+            targets=self._get_param("Targets"),
+            task_arn=self._get_param("TaskArn"),
+            service_role_arn=self._get_param("ServiceRoleArn"),
+            task_type=self._get_param("TaskType"),
+            task_parameters=self._get_param("TaskParameters"),
+            task_invocation_parameters=self._get_param("TaskInvocationParameters"),
+            priority=self._get_param("Priority"),
+            max_concurrency=self._get_param("MaxConcurrency"),
+            max_errors=self._get_param("MaxErrors"),
+            logging_info=self._get_param("LoggingInfo"),
+            name=self._get_param("Name"),
+            description=self._get_param("Description"),
+            cutoff_behavior=self._get_param("CutoffBehavior"),
+            alarm_configurations=self._get_param("AlarmConfigurations"),
+        )
+        return json.dumps({"WindowTaskId": window_task_id})
+
+    def describe_maintenance_window_tasks(self) -> str:
+        window_id = self._get_param("WindowId")
+        filters = self._get_param("Filters", [])
+        tasks = [
+            task.to_json()
+            for task in self.ssm_backend.describe_maintenance_window_tasks(
+                window_id, filters
+            )
+        ]
+        return json.dumps({"Tasks": tasks})
+
+    def deregister_task_from_maintenance_window(self) -> str:
+        window_id = self._get_param("WindowId")
+        window_task_id = self._get_param("WindowTaskId")
+        self.ssm_backend.deregister_task_from_maintenance_window(
+            window_id, window_task_id
+        )
+        return "{}"


Test patch:
diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt
index b23717eed..bd41fd494 100644
--- a/tests/terraformtests/terraform-tests.success.txt
+++ b/tests/terraformtests/terraform-tests.success.txt
@@ -85,6 +85,7 @@ cloudformation:
 cloudfront:
   - TestAccCloudFrontDistributionDataSource_basic
   - TestAccCloudFrontDistribution_isIPV6Enabled
+  - TestAccCloudFrontOriginAccessControl_
 cloudtrail:
   - TestAccCloudTrailServiceAccount
 cloudwatch:
diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py
index e6a1a9e56..1d567ef09 100644
--- a/tests/test_awslambda/test_lambda.py
+++ b/tests/test_awslambda/test_lambda.py
@@ -1,7 +1,7 @@
 import base64
 import json
 import os
-from unittest import SkipTest
+from unittest import SkipTest, mock
 import boto3
 import hashlib
 import pytest
@@ -25,6 +25,7 @@ _lambda_region = "us-west-2"
 boto3.setup_default_session(region_name=_lambda_region)
 
 
+@mock.patch.dict("os.environ", {"MOTO_ENABLE_ISO_REGIONS": "true"})
 @pytest.mark.parametrize("region", ["us-west-2", "cn-northwest-1", "us-isob-east-1"])
 @mock_lambda
 def test_lambda_regions(region):
diff --git a/tests/test_cloudfront/test_cloudfront_oac.py b/tests/test_cloudfront/test_cloudfront_oac.py
new file mode 100644
index 000000000..439f5189a
--- /dev/null
+++ b/tests/test_cloudfront/test_cloudfront_oac.py
@@ -0,0 +1,68 @@
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+from moto import mock_cloudfront
+
+
+@mock_cloudfront
+def test_create_origin_access_control():
+    cf = boto3.client("cloudfront", "us-east-1")
+
+    oac_list = cf.list_origin_access_controls()["OriginAccessControlList"]
+    assert oac_list["Items"] == []
+
+    oac_input = {
+        "Name": "my_oac",
+        "SigningProtocol": "sigv4",
+        "SigningBehavior": "always",
+        "OriginAccessControlOriginType": "s3",
+    }
+    resp = cf.create_origin_access_control(OriginAccessControlConfig=oac_input)[
+        "OriginAccessControl"
+    ]
+    control_id = resp.pop("Id")
+
+    assert control_id is not None
+    assert resp["OriginAccessControlConfig"] == oac_input
+
+    resp = cf.get_origin_access_control(Id=control_id)["OriginAccessControl"]
+    assert resp.pop("Id") is not None
+    assert resp["OriginAccessControlConfig"] == oac_input
+
+    oac_list = cf.list_origin_access_controls()["OriginAccessControlList"]
+    assert oac_list["Items"][0].pop("Id") == control_id
+    assert oac_list["Items"][0] == oac_input
+
+    cf.delete_origin_access_control(Id=control_id)
+
+    oac_list = cf.list_origin_access_controls()["OriginAccessControlList"]
+    assert oac_list["Items"] == []
+
+    with pytest.raises(ClientError) as exc:
+        cf.get_origin_access_control(Id=control_id)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "NoSuchOriginAccessControl"
+    assert err["Message"] == "The specified origin access control does not exist."
+
+
+@mock_cloudfront
+def test_update_origin_access_control():
+    # http://localhost:5000/2020-05-31/origin-access-control/DE53MREVCPIFL/config
+    cf = boto3.client("cloudfront", "us-east-1")
+    oac_input = {
+        "Name": "my_oac",
+        "SigningProtocol": "sigv4",
+        "SigningBehavior": "always",
+        "OriginAccessControlOriginType": "s3",
+    }
+    resp = cf.create_origin_access_control(OriginAccessControlConfig=oac_input)[
+        "OriginAccessControl"
+    ]
+    control_id = resp.pop("Id")
+
+    oac_input["Description"] = "updated"
+    control = cf.update_origin_access_control(
+        Id=control_id, OriginAccessControlConfig=oac_input
+    )["OriginAccessControl"]
+    assert control["Id"] == control_id
+    assert control["OriginAccessControlConfig"] == oac_input
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 8de5cc32f..3b53e3772 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -1753,6 +1753,38 @@ def test_delete_item():
     assert response["Count"] == 0
 
 
+@mock_dynamodb
+def test_delete_item_error():
+    # Setup
+    client = boto3.resource("dynamodb", region_name="us-east-1")
+    # Create the DynamoDB table.
+    client.create_table(
+        TableName="test1",
+        AttributeDefinitions=[
+            {"AttributeName": "client", "AttributeType": "S"},
+            {"AttributeName": "app", "AttributeType": "S"},
+        ],
+        KeySchema=[
+            {"AttributeName": "client", "KeyType": "HASH"},
+            {"AttributeName": "app", "KeyType": "RANGE"},
+        ],
+        BillingMode="PAY_PER_REQUEST",
+    )
+    table = client.Table("test1")
+    table.delete()
+
+    # Execute
+    with pytest.raises(ClientError) as ex:
+        table.delete_item(
+            Key={"client": "client1", "app": "app1"},
+        )
+
+    # Verify
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "Requested resource not found"
+
+
 @mock_dynamodb
 def test_describe_limits():
     client = boto3.client("dynamodb", region_name="eu-central-1")
diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py
index caadf8bd6..780f8f963 100644
--- a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py
+++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py
@@ -197,11 +197,9 @@ def test_delete_item_with_undeclared_table():
             TableName="messages", Key={"forum_name": {"S": "LOLCat Forum"}}
         )
 
-    ex.value.response["Error"]["Code"].should.equal("ConditionalCheckFailedException")
-    ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
-    ex.value.response["Error"]["Message"].should.equal(
-        "A condition specified in the operation could not be evaluated."
-    )
+    assert ex.value.response["Error"]["Code"] == "ResourceNotFoundException"
+    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+    assert ex.value.response["Error"]["Message"] == "Requested resource not found"
 
 
 @mock_dynamodb
diff --git a/tests/test_ssm/test_ssm_maintenance_windows.py b/tests/test_ssm/test_ssm_maintenance_windows.py
index f484a4252..9d0e2ca26 100644
--- a/tests/test_ssm/test_ssm_maintenance_windows.py
+++ b/tests/test_ssm/test_ssm_maintenance_windows.py
@@ -274,3 +274,111 @@ def test_deregister_target_from_maintenance_window():
         WindowId=window_id,
     )
     resp.should.have.key("Targets").should.have.length_of(0)
+
+
+@mock_ssm
+def test_describe_maintenance_window_with_no_task_or_targets():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    window_id = resp["WindowId"]
+
+    resp = ssm.describe_maintenance_window_tasks(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Tasks").should.have.length_of(0)
+
+    resp = ssm.describe_maintenance_window_targets(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Targets").should.have.length_of(0)
+
+
+@mock_ssm
+def test_register_maintenance_window_task():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    window_id = resp["WindowId"]
+
+    resp = ssm.register_target_with_maintenance_window(
+        WindowId=window_id,
+        ResourceType="INSTANCE",
+        Targets=[{"Key": "tag:Name", "Values": ["my-instance"]}],
+    )
+    window_target_id = resp["WindowTargetId"]
+
+    resp = ssm.register_task_with_maintenance_window(
+        WindowId=window_id,
+        Targets=[{"Key": "WindowTargetIds", "Values": [window_target_id]}],
+        TaskArn="AWS-RunShellScript",
+        TaskType="RUN_COMMAND",
+        MaxConcurrency="1",
+        MaxErrors="1",
+    )
+
+    resp.should.have.key("WindowTaskId")
+    _id = resp["WindowTaskId"]
+
+    resp = ssm.describe_maintenance_window_tasks(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Tasks").should.have.length_of(1)
+    resp["Tasks"][0].should.have.key("WindowTaskId").equal(_id)
+    resp["Tasks"][0].should.have.key("WindowId").equal(window_id)
+    resp["Tasks"][0].should.have.key("TaskArn").equal("AWS-RunShellScript")
+    resp["Tasks"][0].should.have.key("MaxConcurrency").equal("1")
+    resp["Tasks"][0].should.have.key("MaxErrors").equal("1")
+
+
+@mock_ssm
+def test_deregister_maintenance_window_task():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    window_id = resp["WindowId"]
+
+    resp = ssm.register_target_with_maintenance_window(
+        WindowId=window_id,
+        ResourceType="INSTANCE",
+        Targets=[{"Key": "tag:Name", "Values": ["my-instance"]}],
+    )
+    window_target_id = resp["WindowTargetId"]
+
+    resp = ssm.register_task_with_maintenance_window(
+        WindowId=window_id,
+        Targets=[{"Key": "WindowTargetIds", "Values": [window_target_id]}],
+        TaskArn="AWS-RunShellScript",
+        TaskType="RUN_COMMAND",
+        MaxConcurrency="1",
+        MaxErrors="1",
+    )
+    window_task_id = resp["WindowTaskId"]
+
+    ssm.deregister_task_from_maintenance_window(
+        WindowId=window_id,
+        WindowTaskId=window_task_id,
+    )
+
+    resp = ssm.describe_maintenance_window_tasks(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Tasks").should.have.length_of(0)
diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py
index 70d8faa11..b383701bc 100644
--- a/tests/test_stepfunctions/test_stepfunctions.py
+++ b/tests/test_stepfunctions/test_stepfunctions.py
@@ -888,6 +888,7 @@ def test_state_machine_get_execution_history_contains_expected_success_events_wh
     execution_history["events"].should.equal(expected_events)
 
 
+@mock.patch.dict("os.environ", {"MOTO_ENABLE_ISO_REGIONS": "true"})
 @pytest.mark.parametrize(
     "test_region", ["us-west-2", "cn-northwest-1", "us-isob-east-1"]
 )
diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py
index 417089cd5..19ca35b4b 100644
--- a/tests/test_sts/test_sts.py
+++ b/tests/test_sts/test_sts.py
@@ -5,6 +5,7 @@ import boto3
 from botocore.client import ClientError
 from datetime import datetime
 from freezegun import freeze_time
+from unittest.mock import patch
 import pytest
 import sure  # noqa # pylint: disable=unused-import
 
@@ -707,6 +708,7 @@ def test_federation_token_with_too_long_policy():
     )
 
 
+@patch.dict("os.environ", {"MOTO_ENABLE_ISO_REGIONS": "true"})
 @pytest.mark.parametrize("region", ["us-west-2", "cn-northwest-1", "us-isob-east-1"])
 @mock_sts
 def test_sts_regions(region):