Repository: getmoto/moto
Pull request #6338 (2023-05-22 20:46:59)

add tags to service-control-policies (organizations client)
add tags to FakePolicy under organizations client. 

Reference: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/organizations/client/tag_resource.html#tag-resource 


Patch:
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
index 4b3373fb7..6eb8d515e 100644
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -436,6 +436,14 @@ class InvalidParameterValueError(EC2ClientError):
         )
 
 
+class InvalidParameterValueErrorTagSpotFleetRequest(EC2ClientError):
+    def __init__(self, resource_type: str):
+        super().__init__(
+            "InvalidParameterValue",
+            f"The value for `ResourceType` must be `spot-fleet-request`, but got `{resource_type}` instead.",
+        )
+
+
 class EmptyTagSpecError(EC2ClientError):
     def __init__(self) -> None:
         super().__init__(
diff --git a/moto/ec2/models/spot_requests.py b/moto/ec2/models/spot_requests.py
index d9242931c..7887d1c35 100644
--- a/moto/ec2/models/spot_requests.py
+++ b/moto/ec2/models/spot_requests.py
@@ -2,6 +2,7 @@ from collections import defaultdict
 from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
 
 from moto.core.common_models import BaseModel, CloudFormationModel
+from moto.ec2.exceptions import InvalidParameterValueErrorTagSpotFleetRequest
 
 if TYPE_CHECKING:
     from moto.ec2.models.instances import Instance
@@ -185,6 +186,7 @@ class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel):
         launch_specs: List[Dict[str, Any]],
         launch_template_config: Optional[List[Dict[str, Any]]],
         instance_interruption_behaviour: Optional[str],
+        tag_specifications: Optional[List[Dict[str, Any]]],
     ):
 
         self.ec2_backend = ec2_backend
@@ -202,6 +204,14 @@ class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel):
 
         self.launch_specs = []
 
+        self.tags = {}
+        if tag_specifications is not None:
+            tags = convert_tag_spec(tag_specifications)
+            for resource_type in tags:
+                if resource_type != "spot-fleet-request":
+                    raise InvalidParameterValueErrorTagSpotFleetRequest(resource_type)
+            self.tags.update(tags)
+
         launch_specs_from_config = []
         for config in launch_template_config or []:
             spec = config["LaunchTemplateSpecification"]
@@ -456,6 +466,7 @@ class SpotRequestBackend:
         launch_specs: List[Dict[str, Any]],
         launch_template_config: Optional[List[Dict[str, Any]]] = None,
         instance_interruption_behaviour: Optional[str] = None,
+        tag_specifications: Optional[List[Dict[str, Any]]] = None,
     ) -> SpotFleetRequest:
 
         spot_fleet_request_id = random_spot_fleet_request_id()
@@ -470,6 +481,7 @@ class SpotRequestBackend:
             launch_specs=launch_specs,
             launch_template_config=launch_template_config,
             instance_interruption_behaviour=instance_interruption_behaviour,
+            tag_specifications=tag_specifications,
         )
         self.spot_fleet_requests[spot_fleet_request_id] = request
         return request
diff --git a/moto/ec2/responses/spot_fleets.py b/moto/ec2/responses/spot_fleets.py
index 254525fbc..a0ddc5f66 100644
--- a/moto/ec2/responses/spot_fleets.py
+++ b/moto/ec2/responses/spot_fleets.py
@@ -57,6 +57,7 @@ class SpotFleets(EC2BaseResponse):
             .get("LaunchTemplateConfigs", {})
             .values()
         )
+        tag_specifications = spot_config.get("TagSpecification")
 
         request = self.ec2_backend.request_spot_fleet(
             spot_price=spot_price,
@@ -66,6 +67,7 @@ class SpotFleets(EC2BaseResponse):
             launch_specs=launch_specs,
             launch_template_config=launch_template_config,
             instance_interruption_behaviour=instance_interruption_behaviour,
+            tag_specifications=tag_specifications,
         )
 
         template = self.response_template(REQUEST_SPOT_FLEET_TEMPLATE)
@@ -89,6 +91,14 @@ DESCRIBE_SPOT_FLEET_TEMPLATE = """<DescribeSpotFleetRequestsResponse xmlns="http
         <item>
             <spotFleetRequestId>{{ request.id }}</spotFleetRequestId>
             <spotFleetRequestState>{{ request.state }}</spotFleetRequestState>
+            <tagSet>
+                {% for key, value in request.tags.get('spot-fleet-request', {}).items() %}
+                <item>
+                    <key>{{ key }}</key>
+                    <value>{{ value }}</value>
+                </item>
+                {% endfor %}
+            </tagSet>
             <spotFleetRequestConfig>
                 {% if request.spot_price %}
                 <spotPrice>{{ request.spot_price }}</spotPrice>
diff --git a/moto/organizations/models.py b/moto/organizations/models.py
index 80362a3d6..aa3ffb3fd 100644
--- a/moto/organizations/models.py
+++ b/moto/organizations/models.py
@@ -200,6 +200,7 @@ class FakePolicy(BaseModel):
         self.organization_id = organization.id
         self.master_account_id = organization.master_account_id
         self.attachments: List[Any] = []
+        self.tags = {tag["Key"]: tag["Value"] for tag in kwargs.get("Tags", [])}
 
         if not FakePolicy.supported_policy_type(self.type):
             raise InvalidInputException("You specified an invalid value.")


Test patch:
diff --git a/tests/test_ec2/test_spot_fleet.py b/tests/test_ec2/test_spot_fleet.py
index 4cd370c85..a4494cd5d 100644
--- a/tests/test_ec2/test_spot_fleet.py
+++ b/tests/test_ec2/test_spot_fleet.py
@@ -4,6 +4,7 @@ import pytest
 
 from moto import mock_ec2
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from botocore.exceptions import ClientError
 from tests import EXAMPLE_AMI_ID
 from uuid import uuid4
 
@@ -64,13 +65,49 @@ def spot_config(subnet_id, allocation_strategy="lowestPrice"):
                 "EbsOptimized": False,
                 "WeightedCapacity": 4.0,
                 "SpotPrice": "10.00",
+                "TagSpecifications": [
+                    {
+                        "ResourceType": "instance",
+                        "Tags": [{"Key": "test", "Value": "value"}],
+                    }
+                ],
             },
         ],
         "AllocationStrategy": allocation_strategy,
         "FulfilledCapacity": 6,
+        "TagSpecifications": [
+            {
+                "ResourceType": "spot-fleet-request",
+                "Tags": [{"Key": "test2", "Value": "value2"}],
+            }
+        ],
     }
 
 
+@mock_ec2
+def test_create_spot_fleet_with_invalid_tag_specifications():
+    conn = boto3.client("ec2", region_name="us-west-2")
+    subnet_id = get_subnet_id(conn)
+
+    config = spot_config(subnet_id)
+    invalid_resource_type = "invalid-resource-type"
+    config["TagSpecifications"] = [
+        {
+            "ResourceType": invalid_resource_type,
+            "Tags": [{"Key": "test2", "Value": "value2"}],
+        }
+    ]
+
+    with pytest.raises(ClientError) as ex:
+        _ = conn.request_spot_fleet(SpotFleetRequestConfig=config)
+
+    ex.value.response["Error"]["Code"].should.equal("InvalidParameterValue")
+    ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    ex.value.response["Error"]["Message"].should.equal(
+        f"The value for `ResourceType` must be `spot-fleet-request`, but got `{invalid_resource_type}` instead."
+    )
+
+
 @mock_ec2
 def test_create_spot_fleet_with_lowest_price():
     conn = boto3.client("ec2", region_name="us-west-2")
@@ -87,6 +124,7 @@ def test_create_spot_fleet_with_lowest_price():
     len(spot_fleet_requests).should.equal(1)
     spot_fleet_request = spot_fleet_requests[0]
     spot_fleet_request["SpotFleetRequestState"].should.equal("active")
+    spot_fleet_request["Tags"].should.equal([{"Key": "test2", "Value": "value2"}])
     spot_fleet_config = spot_fleet_request["SpotFleetRequestConfig"]
 
     spot_fleet_config["SpotPrice"].should.equal("0.12")
diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py
index 5e7e8f85f..7f83dbc2d 100644
--- a/tests/test_organizations/test_organizations_boto3.py
+++ b/tests/test_organizations/test_organizations_boto3.py
@@ -1244,6 +1244,38 @@ def test_tag_resource_organization_organizational_unit():
     response["Tags"].should.equal([])
 
 
+@mock_organizations
+def test_tag_resource_policy():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+    _ = client.list_roots()["Roots"][0]["Id"]
+
+    resource_id = client.create_policy(
+        Content=json.dumps(policy_doc01),
+        Description="A dummy service control policy",
+        Name="MockServiceControlPolicy",
+        Type="SERVICE_CONTROL_POLICY",
+    )["Policy"]["PolicySummary"]["Id"]
+
+    client.tag_resource(ResourceId=resource_id, Tags=[{"Key": "key", "Value": "value"}])
+
+    response = client.list_tags_for_resource(ResourceId=resource_id)
+    response["Tags"].should.equal([{"Key": "key", "Value": "value"}])
+
+    # adding a tag with an existing key, will update the value
+    client.tag_resource(
+        ResourceId=resource_id, Tags=[{"Key": "key", "Value": "new-value"}]
+    )
+
+    response = client.list_tags_for_resource(ResourceId=resource_id)
+    response["Tags"].should.equal([{"Key": "key", "Value": "new-value"}])
+
+    client.untag_resource(ResourceId=resource_id, TagKeys=["key"])
+
+    response = client.list_tags_for_resource(ResourceId=resource_id)
+    response["Tags"].should.equal([])
+
+
 @mock_organizations
 def test_tag_resource_errors():
     client = boto3.client("organizations", region_name="us-east-1")