Repository: getmoto/moto
Pull request #6434 (2023-06-22 22:00:27)

Fixed bug in new Config recorder logic
Fixed logic bug in handling the default empty `exclusionByResourceTypes` dictionary passed in.


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index a91e89a06..42bb497a3 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -6744,7 +6744,7 @@
 - [ ] delete_resource_policy
 - [ ] deregister_managed_instance
 - [ ] deregister_patch_baseline_for_patch_group
-- [ ] deregister_target_from_maintenance_window
+- [X] deregister_target_from_maintenance_window
 - [ ] deregister_task_from_maintenance_window
 - [ ] describe_activations
 - [ ] describe_association
@@ -6767,7 +6767,7 @@
 - [ ] describe_maintenance_window_execution_tasks
 - [ ] describe_maintenance_window_executions
 - [ ] describe_maintenance_window_schedule
-- [ ] describe_maintenance_window_targets
+- [X] describe_maintenance_window_targets
 - [ ] describe_maintenance_window_tasks
 - [X] describe_maintenance_windows
 - [ ] describe_maintenance_windows_for_target
@@ -6828,7 +6828,7 @@
 - [ ] put_resource_policy
 - [ ] register_default_patch_baseline
 - [ ] register_patch_baseline_for_patch_group
-- [ ] register_target_with_maintenance_window
+- [X] register_target_with_maintenance_window
 - [ ] register_task_with_maintenance_window
 - [X] remove_tags_from_resource
 - [ ] reset_service_setting
diff --git a/moto/config/models.py b/moto/config/models.py
index fbed2ab2a..b5e58b84f 100644
--- a/moto/config/models.py
+++ b/moto/config/models.py
@@ -1226,7 +1226,10 @@ class ConfigBackend(BaseBackend):
             if rgroup.get("allSupported", False):
                 if (
                     rgroup.get("resourceTypes", [])
-                    or rgroup.get("exclusionByResourceTypes", {})
+                    or (
+                        rgroup.get("exclusionByResourceTypes", {"resourceTypes": []})
+                        != {"resourceTypes": []}
+                    )
                     or recording_strategy not in {None, "ALL_SUPPORTED_RESOURCE_TYPES"}
                 ):
                     raise InvalidRecordingGroupException()
@@ -1245,7 +1248,10 @@ class ConfigBackend(BaseBackend):
             elif rgroup.get("resourceTypes", []):
                 if (
                     rgroup.get("includeGlobalResourceTypes", False)
-                    or rgroup.get("exclusionByResourceTypes", {})
+                    or (
+                        rgroup.get("exclusionByResourceTypes", {"resourceTypes": []})
+                        != {"resourceTypes": []}
+                    )
                     or recording_strategy not in {None, "INCLUSION_BY_RESOURCE_TYPES"}
                 ):
                     raise InvalidRecordingGroupException()
diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py
index 4c6b69ca6..29813c22d 100644
--- a/moto/ssm/exceptions.py
+++ b/moto/ssm/exceptions.py
@@ -139,3 +139,13 @@ class ParameterMaxVersionLimitExceeded(JsonRESTError):
 
     def __init__(self, message: str):
         super().__init__("ParameterMaxVersionLimitExceeded", message)
+
+
+class ParameterAlreadyExists(JsonRESTError):
+    code = 400
+
+    def __init__(self) -> None:
+        super().__init__(
+            "ParameterAlreadyExists",
+            "The parameter already exists. To overwrite this value, set the overwrite option in the request to true.",
+        )
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index 8b21fd4d7..36865ec1a 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -941,6 +941,48 @@ def _valid_parameter_data_type(data_type: str) -> bool:
     return data_type in ("text", "aws:ec2:image")
 
 
+class FakeMaintenanceWindowTarget:
+    def __init__(
+        self,
+        window_id: str,
+        resource_type: str,
+        targets: List[Dict[str, Any]],
+        owner_information: Optional[str],
+        name: Optional[str],
+        description: Optional[str],
+    ):
+        self.window_id = window_id
+        self.window_target_id = self.generate_id()
+        self.resource_type = resource_type
+        self.targets = targets
+        self.name = name
+        self.description = description
+        self.owner_information = owner_information
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "WindowId": self.window_id,
+            "WindowTargetId": self.window_target_id,
+            "ResourceType": self.resource_type,
+            "Targets": self.targets,
+            "OwnerInformation": "",
+            "Name": self.name,
+            "Description": self.description,
+        }
+
+    @staticmethod
+    def generate_id() -> str:
+        return str(random.uuid4())
+
+
+def _maintenance_window_target_filter_match(
+    filters: Optional[List[Dict[str, Any]]], target: FakeMaintenanceWindowTarget
+) -> bool:
+    if not filters and target:
+        return True
+    return False
+
+
 class FakeMaintenanceWindow:
     def __init__(
         self,
@@ -964,6 +1006,7 @@ class FakeMaintenanceWindow:
         self.schedule_offset = schedule_offset
         self.start_date = start_date
         self.end_date = end_date
+        self.targets: Dict[str, FakeMaintenanceWindowTarget] = {}
 
     def to_json(self) -> Dict[str, Any]:
         return {
@@ -1952,7 +1995,11 @@ class SimpleSystemManagerBackend(BaseBackend):
                 )
             raise ValidationException(invalid_prefix_error)
 
-        if not _valid_parameter_type(parameter_type):
+        if (
+            not _valid_parameter_type(parameter_type)
+            and not overwrite
+            and name not in self._parameters
+        ):
             raise ValidationException(
                 f"1 validation error detected: Value '{parameter_type}' at 'type' failed to satisfy constraint: Member must satisfy enum value set: [SecureString, StringList, String]",
             )
@@ -2250,5 +2297,53 @@ class SimpleSystemManagerBackend(BaseBackend):
         """
         del self.baselines[baseline_id]
 
+    def register_target_with_maintenance_window(
+        self,
+        window_id: str,
+        resource_type: str,
+        targets: List[Dict[str, Any]],
+        owner_information: Optional[str],
+        name: Optional[str],
+        description: Optional[str],
+    ) -> str:
+        """
+        Registers a target with a maintenance window. No error handling has been implemented yet.
+        """
+        window = self.get_maintenance_window(window_id)
+
+        target = FakeMaintenanceWindowTarget(
+            window_id,
+            resource_type,
+            targets,
+            owner_information=owner_information,
+            name=name,
+            description=description,
+        )
+        window.targets[target.window_target_id] = target
+        return target.window_target_id
+
+    def deregister_target_from_maintenance_window(
+        self, window_id: str, window_target_id: str
+    ) -> None:
+        """
+        Deregisters a target from a maintenance window. No error handling has been implemented yet.
+        """
+        window = self.get_maintenance_window(window_id)
+        del window.targets[window_target_id]
+
+    def describe_maintenance_window_targets(
+        self, window_id: str, filters: Optional[List[Dict[str, Any]]]
+    ) -> List[FakeMaintenanceWindowTarget]:
+        """
+        Describes all targets for a maintenance window. No error handling has been implemented yet.
+        """
+        window = self.get_maintenance_window(window_id)
+        targets = [
+            target
+            for target in window.targets.values()
+            if _maintenance_window_target_filter_match(filters, target)
+        ]
+        return targets
+
 
 ssm_backends = BackendDict(SimpleSystemManagerBackend, "ssm")
diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py
index b53ba3c5c..3f757f03d 100644
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -2,7 +2,7 @@ import json
 from typing import Any, Dict, Tuple, Union
 
 from moto.core.responses import BaseResponse
-from .exceptions import ValidationException
+from .exceptions import ValidationException, ParameterAlreadyExists
 from .models import ssm_backends, SimpleSystemManagerBackend
 
 
@@ -286,11 +286,7 @@ class SimpleSystemManagerResponse(BaseResponse):
         )
 
         if result is None:
-            error = {
-                "__type": "ParameterAlreadyExists",
-                "message": f"Parameter {name} already exists.",
-            }
-            return json.dumps(error), dict(status=400)
+            raise ParameterAlreadyExists
 
         response = {"Version": result}
         return json.dumps(response)
@@ -441,6 +437,36 @@ class SimpleSystemManagerResponse(BaseResponse):
         window = self.ssm_backend.get_maintenance_window(window_id)
         return json.dumps(window.to_json())
 
+    def register_target_with_maintenance_window(self) -> str:
+        window_target_id = self.ssm_backend.register_target_with_maintenance_window(
+            window_id=self._get_param("WindowId"),
+            resource_type=self._get_param("ResourceType"),
+            targets=self._get_param("Targets"),
+            owner_information=self._get_param("OwnerInformation"),
+            name=self._get_param("Name"),
+            description=self._get_param("Description"),
+        )
+        return json.dumps({"WindowTargetId": window_target_id})
+
+    def describe_maintenance_window_targets(self) -> str:
+        window_id = self._get_param("WindowId")
+        filters = self._get_param("Filters", [])
+        targets = [
+            target.to_json()
+            for target in self.ssm_backend.describe_maintenance_window_targets(
+                window_id, filters
+            )
+        ]
+        return json.dumps({"Targets": targets})
+
+    def deregister_target_from_maintenance_window(self) -> str:
+        window_id = self._get_param("WindowId")
+        window_target_id = self._get_param("WindowTargetId")
+        self.ssm_backend.deregister_target_from_maintenance_window(
+            window_id, window_target_id
+        )
+        return "{}"
+
     def describe_maintenance_windows(self) -> str:
         filters = self._get_param("Filters", None)
         windows = [


Test patch:
diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py
index 966881ff4..5bf6e5b22 100644
--- a/tests/test_config/test_config.py
+++ b/tests/test_config/test_config.py
@@ -95,6 +95,10 @@ def test_put_configuration_recorder():
             "exclusionByResourceTypes": {"resourceTypes": []},
             "recordingStrategy": {"useOnly": "EXCLUSION_BY_RESOURCE_TYPES"},
         },
+        {
+            "resourceTypes": ["AWS::S3::Bucket"],
+            "exclusionByResourceTypes": {"resourceTypes": ["AWS::S3::Bucket"]},
+        },
         {"includeGlobalResourceTypes": False, "resourceTypes": []},
         {"includeGlobalResourceTypes": True},
         {"resourceTypes": []},
@@ -199,6 +203,7 @@ def test_put_configuration_recorder():
                 "allSupported": False,
                 "includeGlobalResourceTypes": False,
                 "resourceTypes": ["AWS::EC2::Volume", "AWS::EC2::VPC"],
+                "exclusionByResourceTypes": {"resourceTypes": []},
             },
         }
     )
@@ -256,6 +261,24 @@ def test_put_configuration_recorder():
         "recordingGroup"
     ]["allSupported"]
 
+    # Try this again, but this time, supply all the excess fields as empty:
+    client.put_configuration_recorder(
+        ConfigurationRecorder={
+            "name": "testrecorder",
+            "roleARN": "somearn",
+            "recordingGroup": {
+                "allSupported": True,
+                "includeGlobalResourceTypes": True,
+                "resourceTypes": [],
+                "exclusionByResourceTypes": {"resourceTypes": []},
+                "recordingStrategy": {"useOnly": "ALL_SUPPORTED_RESOURCE_TYPES"},
+            },
+        }
+    )
+    assert client.describe_configuration_recorders()["ConfigurationRecorders"][0][
+        "recordingGroup"
+    ]["allSupported"]
+
     # Update again for exclusions:
     client.put_configuration_recorder(
         ConfigurationRecorder={
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
index daf6a73a9..6c227ee8d 100644
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -253,7 +253,7 @@ def test_put_parameter(name):
     except botocore.exceptions.ClientError as err:
         err.operation_name.should.equal("PutParameter")
         err.response["Error"]["Message"].should.equal(
-            f"Parameter {name} already exists."
+            "The parameter already exists. To overwrite this value, set the overwrite option in the request to true."
         )
 
     response = client.get_parameters(Names=[name], WithDecryption=False)
@@ -431,6 +431,61 @@ def test_put_parameter_invalid_type():
     )
 
 
+@mock_ssm
+def test_update_parameter():
+    # Setup
+    client = boto3.client("ssm", "us-east-1")
+    param_name = "test_param"
+    updated_value = "UpdatedValue"
+    client.put_parameter(
+        Description="Description",
+        Name=param_name,
+        Type="String",
+        Value="Value",
+    )
+
+    # Execute
+    response = client.put_parameter(
+        Name=param_name,
+        Overwrite=True,
+        Value=updated_value,
+    )
+    new_param = client.get_parameter(Name=param_name)
+
+    # Verify
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert new_param["Parameter"]["Value"] == updated_value
+
+
+@mock_ssm
+def test_update_parameter_already_exists_error():
+    # Setup
+    client = boto3.client("ssm", "us-east-1")
+    client.put_parameter(
+        Description="Description",
+        Name="Name",
+        Type="String",
+        Value="Value",
+    )
+
+    # Execute
+    with pytest.raises(ClientError) as exc:
+        client.put_parameter(
+            Name="Name",
+            Value="UpdatedValue",
+        )
+
+    # Verify
+    ex = exc.value
+    assert ex.operation_name.should.equal("PutParameter")
+    assert ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert ex.response["Error"]["Code"] == "ParameterAlreadyExists"
+    assert (
+        ex.response["Error"]["Message"]
+        == "The parameter already exists. To overwrite this value, set the overwrite option in the request to true."
+    )
+
+
 @mock_ssm
 def test_get_parameter():
     client = boto3.client("ssm", region_name="us-east-1")
diff --git a/tests/test_ssm/test_ssm_maintenance_windows.py b/tests/test_ssm/test_ssm_maintenance_windows.py
index 08b3c1ddc..f484a4252 100644
--- a/tests/test_ssm/test_ssm_maintenance_windows.py
+++ b/tests/test_ssm/test_ssm_maintenance_windows.py
@@ -212,3 +212,65 @@ def test_tags():
         ResourceType="MaintenanceWindow", ResourceId=mw_id
     )["TagList"]
     assert tags == [{"Key": "k2", "Value": "v2"}]
+
+
+@mock_ssm
+def test_register_maintenance_window_target():
+    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"]}],
+    )
+    resp.should.have.key("WindowTargetId")
+    _id = resp["WindowTargetId"]
+
+    resp = ssm.describe_maintenance_window_targets(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Targets").should.have.length_of(1)
+    resp["Targets"][0].should.have.key("ResourceType").equal("INSTANCE")
+    resp["Targets"][0].should.have.key("WindowTargetId").equal(_id)
+    resp["Targets"][0]["Targets"][0].should.have.key("Key").equal("tag:Name")
+    resp["Targets"][0]["Targets"][0].should.have.key("Values").equal(["my-instance"])
+
+
+@mock_ssm
+def test_deregister_target_from_maintenance_window():
+    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"]}],
+    )
+    _id = resp["WindowTargetId"]
+
+    ssm.deregister_target_from_maintenance_window(
+        WindowId=window_id,
+        WindowTargetId=_id,
+    )
+
+    resp = ssm.describe_maintenance_window_targets(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Targets").should.have.length_of(0)