EC2: Add validations for create_subnet tags (#6860)
This commit is contained in:
		
							parent
							
								
									24d9ea61ce
								
							
						
					
					
						commit
						197e8710af
					
				| @ -749,6 +749,14 @@ class GenericInvalidParameterValueError(EC2ClientError): | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class InvalidParameter(EC2ClientError): | ||||||
|  |     def __init__(self, message: str): | ||||||
|  |         super().__init__( | ||||||
|  |             "InvalidParameter", | ||||||
|  |             message, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class InvalidSubnetCidrBlockAssociationID(EC2ClientError): | class InvalidSubnetCidrBlockAssociationID(EC2ClientError): | ||||||
|     def __init__(self, association_id: str): |     def __init__(self, association_id: str): | ||||||
|         super().__init__( |         super().__init__( | ||||||
|  | |||||||
| @ -317,8 +317,9 @@ class SubnetBackend: | |||||||
|         ipv6_cidr_block: Optional[str] = None, |         ipv6_cidr_block: Optional[str] = None, | ||||||
|         availability_zone: Optional[str] = None, |         availability_zone: Optional[str] = None, | ||||||
|         availability_zone_id: Optional[str] = None, |         availability_zone_id: Optional[str] = None, | ||||||
|         tags: Optional[List[Dict[str, str]]] = None, |         tags: Optional[Dict[str, Dict[str, str]]] = None, | ||||||
|     ) -> Subnet: |     ) -> Subnet: | ||||||
|  | 
 | ||||||
|         subnet_id = random_subnet_id() |         subnet_id = random_subnet_id() | ||||||
|         # Validate VPC exists and the supplied CIDR block is a subnet of the VPC's |         # Validate VPC exists and the supplied CIDR block is a subnet of the VPC's | ||||||
|         vpc = self.get_vpc(vpc_id)  # type: ignore[attr-defined] |         vpc = self.get_vpc(vpc_id)  # type: ignore[attr-defined] | ||||||
| @ -400,8 +401,8 @@ class SubnetBackend: | |||||||
|             assign_ipv6_address_on_creation=False, |             assign_ipv6_address_on_creation=False, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         for tag in tags or []: |         for k, v in tags.get("subnet", {}).items() if tags else []: | ||||||
|             subnet.add_tag(tag["Key"], tag["Value"]) |             subnet.add_tag(k, v) | ||||||
| 
 | 
 | ||||||
|         # AWS associates a new subnet with the default Network ACL |         # AWS associates a new subnet with the default Network ACL | ||||||
|         self.associate_default_network_acl_with_subnet(subnet_id, vpc_id)  # type: ignore[attr-defined] |         self.associate_default_network_acl_with_subnet(subnet_id, vpc_id)  # type: ignore[attr-defined] | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| from typing import Any, Dict | from typing import Any, Dict, Optional | ||||||
| from moto.core.responses import BaseResponse | from moto.core.responses import BaseResponse | ||||||
| from ..exceptions import EmptyTagSpecError | from ..exceptions import EmptyTagSpecError, InvalidParameter | ||||||
| from ..utils import convert_tag_spec | from ..utils import convert_tag_spec | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -17,15 +17,32 @@ class EC2BaseResponse(BaseResponse): | |||||||
|         # return {x1: y1, ...} |         # return {x1: y1, ...} | ||||||
|         return {f["Name"]: f["Value"] for f in _filters} |         return {f["Name"]: f["Value"] for f in _filters} | ||||||
| 
 | 
 | ||||||
|     def _parse_tag_specification(self) -> Dict[str, Dict[str, str]]: |     def _parse_tag_specification( | ||||||
|  |         self, expected_type: Optional[str] = None | ||||||
|  |     ) -> Dict[str, Dict[str, str]]: | ||||||
|         # [{"ResourceType": _type, "Tag": [{"Key": k, "Value": v}, ..]}] |         # [{"ResourceType": _type, "Tag": [{"Key": k, "Value": v}, ..]}] | ||||||
|         tag_spec_set = self._get_multi_param("TagSpecification") |         tag_spec_set = self._get_multi_param( | ||||||
|  |             "TagSpecification", skip_result_conversion=True | ||||||
|  |         ) | ||||||
|         if not tag_spec_set: |         if not tag_spec_set: | ||||||
|             tag_spec_set = self._get_multi_param("TagSpecifications") |             tag_spec_set = self._get_multi_param( | ||||||
|         # If we do not pass any Tags, this method will convert this to [_type] instead |                 "TagSpecifications", skip_result_conversion=True | ||||||
|         if isinstance(tag_spec_set, list) and any( |             ) | ||||||
|             [isinstance(spec, str) for spec in tag_spec_set] |         if not tag_spec_set: | ||||||
|         ): |             return {} | ||||||
|  | 
 | ||||||
|  |         tags_dict = ( | ||||||
|  |             tag_spec_set[0] if isinstance(tag_spec_set, list) else tag_spec_set | ||||||
|  |         )  # awscli allows for a json string to be passed and it should be allowed | ||||||
|  |         if "ResourceType" not in tags_dict: | ||||||
|  |             raise InvalidParameter("Tag specification resource type must have a value") | ||||||
|  |         if expected_type and tags_dict["ResourceType"] != expected_type: | ||||||
|  |             raise InvalidParameter( | ||||||
|  |                 f"'{tags_dict['ResourceType']}' is not a valid taggable resource type for this operation." | ||||||
|  |             ) | ||||||
|  |         if "Tag" not in tags_dict: | ||||||
|  |             if tags_dict.get("ResourceType") == "subnet": | ||||||
|  |                 raise InvalidParameter("Tag specification must have at least one tag") | ||||||
|             raise EmptyTagSpecError |             raise EmptyTagSpecError | ||||||
|         # {_type: {k: v, ..}} | 
 | ||||||
|         return convert_tag_spec(tag_spec_set) |         return convert_tag_spec(tag_spec_set) | ||||||
|  | |||||||
| @ -11,9 +11,7 @@ class Subnets(EC2BaseResponse): | |||||||
|         ipv6_cidr_block = self._get_param("Ipv6CidrBlock") |         ipv6_cidr_block = self._get_param("Ipv6CidrBlock") | ||||||
|         availability_zone = self._get_param("AvailabilityZone") |         availability_zone = self._get_param("AvailabilityZone") | ||||||
|         availability_zone_id = self._get_param("AvailabilityZoneId") |         availability_zone_id = self._get_param("AvailabilityZoneId") | ||||||
|         tags = self._get_multi_param("TagSpecification") |         tags = self._parse_tag_specification("subnet") | ||||||
|         if tags: |  | ||||||
|             tags = tags[0].get("Tag") |  | ||||||
| 
 | 
 | ||||||
|         if not availability_zone and not availability_zone_id: |         if not availability_zone and not availability_zone_id: | ||||||
|             availability_zone = random.choice( |             availability_zone = random.choice( | ||||||
|  | |||||||
| @ -468,6 +468,67 @@ def test_retrieve_resource_with_multiple_tags(): | |||||||
|     assert blue_instances == [blue] |     assert blue_instances == [blue] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @mock_ec2 | ||||||
|  | def test_ec2_validate_subnet_tags(): | ||||||
|  |     client = boto3.client("ec2", region_name="us-west-1") | ||||||
|  | 
 | ||||||
|  |     # create vpc | ||||||
|  |     vpc = client.create_vpc(CidrBlock="10.0.0.0/16") | ||||||
|  |     vpc_id = vpc["Vpc"]["VpcId"] | ||||||
|  | 
 | ||||||
|  |     with pytest.raises(ClientError) as ex: | ||||||
|  |         client.create_subnet( | ||||||
|  |             VpcId=vpc_id, | ||||||
|  |             CidrBlock="10.0.0.1/24", | ||||||
|  |             TagSpecifications=[{"Tags": [{"Key": "TEST_TAG", "Value": "TEST_VALUE"}]}], | ||||||
|  |         ) | ||||||
|  |     assert ex.value.response["Error"]["Code"] == "InvalidParameter" | ||||||
|  |     assert ( | ||||||
|  |         ex.value.response["Error"]["Message"] | ||||||
|  |         == "Tag specification resource type must have a value" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     with pytest.raises(ClientError) as ex: | ||||||
|  |         client.create_subnet( | ||||||
|  |             VpcId=vpc_id, | ||||||
|  |             CidrBlock="10.0.0.1/24", | ||||||
|  |             TagSpecifications=[{"ResourceType": "subnet"}], | ||||||
|  |         ) | ||||||
|  |     assert ex.value.response["Error"]["Code"] == "InvalidParameter" | ||||||
|  |     assert ( | ||||||
|  |         ex.value.response["Error"]["Message"] | ||||||
|  |         == "Tag specification must have at least one tag" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     with pytest.raises(ClientError) as ex: | ||||||
|  |         client.create_subnet( | ||||||
|  |             VpcId=vpc_id, | ||||||
|  |             CidrBlock="10.0.0.1/24", | ||||||
|  |             TagSpecifications=[ | ||||||
|  |                 { | ||||||
|  |                     "ResourceType": "snapshot", | ||||||
|  |                     "Tags": [{"Key": "TEST_TAG", "Value": "TEST_VALUE"}], | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |         ) | ||||||
|  |     assert ex.value.response["Error"]["Code"] == "InvalidParameter" | ||||||
|  |     assert ( | ||||||
|  |         ex.value.response["Error"]["Message"] | ||||||
|  |         == "'snapshot' is not a valid taggable resource type for this operation." | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     client.create_subnet( | ||||||
|  |         VpcId=vpc_id, | ||||||
|  |         CidrBlock="10.0.0.1/24", | ||||||
|  |         TagSpecifications=[ | ||||||
|  |             { | ||||||
|  |                 "ResourceType": "subnet", | ||||||
|  |                 "Tags": [{"Key": "TEST_TAG", "Value": "TEST_VALUE"}], | ||||||
|  |             } | ||||||
|  |         ], | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def get_filter(tag_val): | def get_filter(tag_val): | ||||||
|     return [ |     return [ | ||||||
|         {"Name": "tag-key", "Values": ["application"]}, |         {"Name": "tag-key", "Values": ["application"]}, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user