diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index c57989431..c731d8120 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -30,6 +30,10 @@ DEFAULT_COOLDOWN = 300 ASG_NAME_TAG = "aws:autoscaling:groupName" +def make_int(value: Union[None, str, int]) -> Optional[int]: + return int(value) if value is not None else value + + class InstanceState: def __init__( self, @@ -406,13 +410,13 @@ class FakeAutoScalingGroup(CloudFormationModel): min_size: Optional[int], launch_config_name: str, launch_template: Dict[str, Any], - vpc_zone_identifier: str, + vpc_zone_identifier: Optional[str], default_cooldown: Optional[int], health_check_period: Optional[int], health_check_type: Optional[str], load_balancers: List[str], target_group_arns: List[str], - placement_group: str, + placement_group: Optional[str], termination_policies: List[str], autoscaling_backend: "AutoScalingBackend", ec2_backend: EC2Backend, @@ -941,9 +945,6 @@ class AutoScalingBackend(BaseBackend): def delete_launch_configuration(self, launch_configuration_name: str) -> None: self.launch_configurations.pop(launch_configuration_name, None) - def make_int(self, value: Union[None, str, int]) -> Optional[int]: - return int(value) if value is not None else value - def put_scheduled_update_group_action( self, name: str, @@ -955,9 +956,9 @@ class AutoScalingBackend(BaseBackend): end_time: str, recurrence: str, ) -> FakeScheduledAction: - max_size = self.make_int(max_size) - min_size = self.make_int(min_size) - desired_capacity = self.make_int(desired_capacity) + max_size = make_int(max_size) + min_size = make_int(min_size) + desired_capacity = make_int(desired_capacity) scheduled_action = FakeScheduledAction( name=name, @@ -1010,13 +1011,13 @@ class AutoScalingBackend(BaseBackend): min_size: Union[None, str, int], launch_config_name: str, launch_template: Dict[str, Any], - vpc_zone_identifier: str, + vpc_zone_identifier: Optional[str], default_cooldown: Optional[int], health_check_period: Union[None, str, int], health_check_type: Optional[str], load_balancers: List[str], target_group_arns: List[str], - placement_group: str, + placement_group: Optional[str], termination_policies: List[str], tags: List[Dict[str, str]], capacity_rebalance: bool = False, @@ -1024,10 +1025,10 @@ class AutoScalingBackend(BaseBackend): instance_id: Optional[str] = None, mixed_instance_policy: Optional[Dict[str, Any]] = None, ) -> FakeAutoScalingGroup: - max_size = self.make_int(max_size) - min_size = self.make_int(min_size) - desired_capacity = self.make_int(desired_capacity) - default_cooldown = self.make_int(default_cooldown) + max_size = make_int(max_size) + min_size = make_int(min_size) + desired_capacity = make_int(desired_capacity) + default_cooldown = make_int(default_cooldown) # Verify only a single launch config-like parameter is provided. params = [ @@ -1064,9 +1065,7 @@ class AutoScalingBackend(BaseBackend): launch_template=launch_template, vpc_zone_identifier=vpc_zone_identifier, default_cooldown=default_cooldown, - health_check_period=self.make_int(health_check_period) - if health_check_period - else 300, + health_check_period=make_int(health_check_period or 300), health_check_type=health_check_type, load_balancers=load_balancers, target_group_arns=target_group_arns, diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 357f6a30c..18e216dd0 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -539,7 +539,7 @@ class LambdaFunction(CloudFormationModel, DockerModel): self.run_time = spec.get("Runtime") self.logs_backend = logs_backends[account_id][self.region] self.environment_vars = spec.get("Environment", {}).get("Variables", {}) - self.policy: Optional[Policy] = None + self.policy = Policy(self) self.url_config: Optional[FunctionUrlConfig] = None self.state = "Active" self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None) @@ -582,7 +582,7 @@ class LambdaFunction(CloudFormationModel, DockerModel): self._set_function_code(self.code) - self.function_arn = make_function_arn( + self.function_arn: str = make_function_arn( self.region, self.account_id, self.function_name ) @@ -659,7 +659,11 @@ class LambdaFunction(CloudFormationModel, DockerModel): ] if not all(layer_versions): raise UnknownLayerVersionException(layers_versions_arns) - return [{"Arn": lv.arn, "CodeSize": lv.code_size} for lv in layer_versions] + # The `if lv` part is not necessary - we know there are no None's, because of the `all()`-check earlier + # But MyPy does not seem to understand this + # The `type: ignore` is because `code_size` is an int, and we're returning Dict[str, str] + # We should convert the return-type into a TypedDict the moment we drop Py3.7 support + return [{"Arn": lv.arn, "CodeSize": lv.code_size} for lv in layer_versions if lv] # type: ignore def get_code_signing_config(self) -> Dict[str, Any]: return { @@ -1577,8 +1581,6 @@ class LambdaStorage(object): self._functions[fn.function_name]["latest"] = fn else: self._functions[fn.function_name] = {"latest": fn, "versions": []} - # instantiate a new policy for this version of the lambda - fn.policy = Policy(fn) self._arns[fn.function_arn] = fn def publish_function( diff --git a/moto/awslambda/policy.py b/moto/awslambda/policy.py index 2490e43f9..793bf8684 100644 --- a/moto/awslambda/policy.py +++ b/moto/awslambda/policy.py @@ -5,14 +5,17 @@ from moto.awslambda.exceptions import ( UnknownPolicyException, ) from moto.moto_api._internal import mock_random -from typing import Any, Callable, Dict, List, Optional, TypeVar +from typing import Any, Callable, Dict, List, Optional, TypeVar, TYPE_CHECKING + +if TYPE_CHECKING: + from .models import LambdaFunction TYPE_IDENTITY = TypeVar("TYPE_IDENTITY") class Policy: - def __init__(self, parent: Any): # Parent should be a LambdaFunction + def __init__(self, parent: "LambdaFunction"): self.revision = str(mock_random.uuid4()) self.statements: List[Dict[str, Any]] = [] self.parent = parent @@ -72,8 +75,6 @@ class Policy: # converts AddPermission request to PolicyStatement # https://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html def decode_policy(self, obj: Dict[str, Any]) -> "Policy": - # import pydevd - # pydevd.settrace("localhost", port=5678) policy = Policy(self.parent) policy.revision = obj.get("RevisionId", "") diff --git a/moto/batch/models.py b/moto/batch/models.py index 20f280e7e..3ad34053a 100644 --- a/moto/batch/models.py +++ b/moto/batch/models.py @@ -114,16 +114,13 @@ class ComputeEnvironment(CloudFormationModel): backend = batch_backends[account_id][region_name] properties = cloudformation_json["Properties"] - env = backend.create_compute_environment( + return backend.create_compute_environment( resource_name, properties["Type"], properties.get("State", "ENABLED"), lowercase_first_key(properties["ComputeResources"]), properties["ServiceRole"], ) - arn = env[1] - - return backend.get_compute_environment_by_arn(arn) class JobQueue(CloudFormationModel): @@ -209,16 +206,13 @@ class JobQueue(CloudFormationModel): for dict_item in properties["ComputeEnvironmentOrder"] ] - queue = backend.create_job_queue( + return backend.create_job_queue( queue_name=resource_name, priority=properties["Priority"], state=properties.get("State", "ENABLED"), compute_env_order=compute_envs, schedule_policy={}, ) - arn = queue[1] - - return backend.get_job_queue_by_arn(arn) class JobDefinition(CloudFormationModel): @@ -447,7 +441,7 @@ class JobDefinition(CloudFormationModel): ) -> "JobDefinition": backend = batch_backends[account_id][region_name] properties = cloudformation_json["Properties"] - res = backend.register_job_definition( + return backend.register_job_definition( def_name=resource_name, parameters=lowercase_first_key(properties.get("Parameters", {})), _type="container", @@ -467,9 +461,6 @@ class JobDefinition(CloudFormationModel): platform_capabilities=None, propagate_tags=None, ) - arn = res[1] - - return backend.get_job_definition_by_arn(arn) class Job(threading.Thread, BaseModel, DockerModel, ManagedState): @@ -1219,7 +1210,7 @@ class BatchBackend(BaseBackend): state: str, compute_resources: Dict[str, Any], service_role: str, - ) -> Tuple[str, str]: + ) -> ComputeEnvironment: # Validate if COMPUTE_ENVIRONMENT_NAME_REGEX.match(compute_environment_name) is None: raise InvalidParameterValueException( @@ -1304,7 +1295,7 @@ class BatchBackend(BaseBackend): ecs_cluster = self.ecs_backend.create_cluster(cluster_name) new_comp_env.set_ecs(ecs_cluster.arn, cluster_name) - return compute_environment_name, new_comp_env.arn + return new_comp_env def _validate_compute_resources(self, cr: Dict[str, Any]) -> None: """ @@ -1504,7 +1495,7 @@ class BatchBackend(BaseBackend): state: str, compute_env_order: List[Dict[str, str]], tags: Optional[Dict[str, str]] = None, - ) -> Tuple[str, str]: + ) -> JobQueue: for variable, var_name in ( (queue_name, "jobQueueName"), (priority, "priority"), @@ -1550,7 +1541,7 @@ class BatchBackend(BaseBackend): ) self._job_queues[queue.arn] = queue - return queue_name, queue.arn + return queue def describe_job_queues( self, job_queues: Optional[List[str]] = None @@ -1644,7 +1635,7 @@ class BatchBackend(BaseBackend): timeout: Dict[str, int], platform_capabilities: List[str], propagate_tags: bool, - ) -> Tuple[str, str, int]: + ) -> JobDefinition: if def_name is None: raise ClientException("jobDefinitionName must be provided") @@ -1683,7 +1674,7 @@ class BatchBackend(BaseBackend): self._job_definitions[job_def.arn] = job_def - return def_name, job_def.arn, job_def.revision + return job_def def deregister_job_definition(self, def_name: str) -> None: job_def = self.get_job_definition_by_arn(def_name) diff --git a/moto/batch/responses.py b/moto/batch/responses.py index 558fcdfa6..8a0ab4675 100644 --- a/moto/batch/responses.py +++ b/moto/batch/responses.py @@ -31,7 +31,7 @@ class BatchResponse(BaseResponse): state = self._get_param("state") _type = self._get_param("type") - name, arn = self.batch_backend.create_compute_environment( + env = self.batch_backend.create_compute_environment( compute_environment_name=compute_env_name, _type=_type, state=state, @@ -39,7 +39,7 @@ class BatchResponse(BaseResponse): service_role=service_role, ) - result = {"computeEnvironmentArn": arn, "computeEnvironmentName": name} + result = {"computeEnvironmentArn": env.arn, "computeEnvironmentName": env.name} return json.dumps(result) @@ -91,7 +91,7 @@ class BatchResponse(BaseResponse): state = self._get_param("state") tags = self._get_param("tags") - name, arn = self.batch_backend.create_job_queue( + queue = self.batch_backend.create_job_queue( queue_name=queue_name, priority=priority, schedule_policy=schedule_policy, @@ -100,7 +100,7 @@ class BatchResponse(BaseResponse): tags=tags, ) - result = {"jobQueueArn": arn, "jobQueueName": name} + result = {"jobQueueArn": queue.arn, "jobQueueName": queue.name} return json.dumps(result) @@ -157,7 +157,7 @@ class BatchResponse(BaseResponse): timeout = self._get_param("timeout") platform_capabilities = self._get_param("platformCapabilities") propagate_tags = self._get_param("propagateTags") - name, arn, revision = self.batch_backend.register_job_definition( + job_def = self.batch_backend.register_job_definition( def_name=def_name, parameters=parameters, _type=_type, @@ -171,9 +171,9 @@ class BatchResponse(BaseResponse): ) result = { - "jobDefinitionArn": arn, - "jobDefinitionName": name, - "revision": revision, + "jobDefinitionArn": job_def.arn, + "jobDefinitionName": job_def.name, + "revision": job_def.revision, } return json.dumps(result) diff --git a/moto/cloudtrail/models.py b/moto/cloudtrail/models.py index da0b855a2..686d39727 100644 --- a/moto/cloudtrail/models.py +++ b/moto/cloudtrail/models.py @@ -140,7 +140,7 @@ class Trail(BaseModel): ) def check_topic_exists(self) -> None: - if self.sns_topic_name: + if self.topic_arn: from moto.sns import sns_backends sns_backend = sns_backends[self.account_id][self.region_name] diff --git a/moto/ecr/models.py b/moto/ecr/models.py index 3de2ceb59..4a41e8eea 100644 --- a/moto/ecr/models.py +++ b/moto/ecr/models.py @@ -69,7 +69,7 @@ class Repository(BaseObject, CloudFormationModel): account_id: str, region_name: str, repository_name: str, - registry_id: str, + registry_id: Optional[str], encryption_config: Optional[Dict[str, str]], image_scan_config: str, image_tag_mutablility: str, @@ -467,7 +467,7 @@ class ECRBackend(BaseBackend): def create_repository( self, repository_name: str, - registry_id: str, + registry_id: Optional[str], encryption_config: Dict[str, str], image_scan_config: Any, image_tag_mutablility: str, diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 41f122be7..ba34ea05b 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -693,7 +693,9 @@ class Service(BaseObject, CloudFormationModel): ): # TODO: LoadBalancers # TODO: Role - ecs_backend.delete_service(cluster_name, service_name) + ecs_backend.delete_service( + original_resource.cluster_name, service_name, force=True + ) return ecs_backend.create_service( cluster_name, new_resource_name, diff --git a/moto/elb/models.py b/moto/elb/models.py index e70faeafc..5c9837939 100644 --- a/moto/elb/models.py +++ b/moto/elb/models.py @@ -169,16 +169,22 @@ class FakeLoadBalancer(CloudFormationModel): port_policies: Dict[str, Any] = {} for policy in policies: policy_name = policy["PolicyName"] - other_policy = OtherPolicy(policy_name, "", []) - elb_backend.create_lb_other_policy(new_elb.name, other_policy) + policy_type_name = policy["PolicyType"] + policy_attrs = policy["Attributes"] + elb_backend.create_load_balancer_policy( + load_balancer_name=new_elb.name, + policy_name=policy_name, + policy_type_name=policy_type_name, + policy_attrs=policy_attrs, + ) for port in policy.get("InstancePorts", []): policies_for_port: Any = port_policies.get(port, set()) policies_for_port.add(policy_name) port_policies[port] = policies_for_port for port, policies in port_policies.items(): - elb_backend.set_load_balancer_policies_of_backend_server( - new_elb.name, port, list(policies) + elb_backend.set_load_balancer_policies_for_backend_server( + new_elb.name, int(port), list(policies) ) health_check = properties.get("HealthCheck") @@ -552,7 +558,7 @@ class ELBBackend(BaseBackend): if access_log: load_balancer.attributes["access_log"] = access_log - def create_lb_other_policy( + def create_load_balancer_policy( self, load_balancer_name: str, policy_name: str, @@ -586,7 +592,7 @@ class ELBBackend(BaseBackend): load_balancer.policies.append(policy) return load_balancer - def set_load_balancer_policies_of_backend_server( + def set_load_balancer_policies_for_backend_server( self, load_balancer_name: str, instance_port: int, policies: List[str] ) -> FakeLoadBalancer: load_balancer = self.get_load_balancer(load_balancer_name) diff --git a/moto/elb/responses.py b/moto/elb/responses.py index fb9783f07..d772948ae 100644 --- a/moto/elb/responses.py +++ b/moto/elb/responses.py @@ -200,7 +200,7 @@ class ELBResponse(BaseResponse): policy_type_name = self._get_param("PolicyTypeName") policy_attrs = self._get_multi_param("PolicyAttributes.member.") - self.elb_backend.create_lb_other_policy( + self.elb_backend.create_load_balancer_policy( load_balancer_name, policy_name, policy_type_name, policy_attrs ) @@ -269,7 +269,7 @@ class ELBResponse(BaseResponse): ] if mb_backend: policies = self._get_multi_param("PolicyNames.member") - self.elb_backend.set_load_balancer_policies_of_backend_server( + self.elb_backend.set_load_balancer_policies_for_backend_server( load_balancer_name, instance_port, policies ) # else: explode? diff --git a/moto/iam/models.py b/moto/iam/models.py index 3fb270f8e..f1fc5ead4 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1075,6 +1075,9 @@ class AccessKeyLastUsed: def timestamp(self) -> str: return iso_8601_datetime_without_milliseconds(self._timestamp) # type: ignore + def strftime(self, date_format: str) -> str: + return self._timestamp.strftime(date_format) + class AccessKey(CloudFormationModel): def __init__( @@ -1091,7 +1094,15 @@ class AccessKey(CloudFormationModel): self.secret_access_key = random_alphanumeric(40) self.status = status self.create_date = utcnow() - self.last_used: Optional[datetime] = None + + # Some users will set this field manually + # And they will be setting this value to a `datetime` + # https://github.com/getmoto/moto/issues/5927#issuecomment-1738188283 + # + # The `to_csv` method calls `last_used.strptime`, which currently works on both AccessKeyLastUsed and datetime + # In the next major release we should communicate that this only accepts AccessKeyLastUsed + # (And rework to_csv accordingly) + self.last_used: Optional[AccessKeyLastUsed] = None self.role_arn: Optional[str] = None @property diff --git a/moto/iot/models.py b/moto/iot/models.py index 9696784a6..d35e0e3cc 100644 --- a/moto/iot/models.py +++ b/moto/iot/models.py @@ -1244,7 +1244,7 @@ class IoTBackend(BaseBackend): cognito = cognitoidentity_backends[self.account_id][self.region_name] identities = [] for identity_pool in cognito.identity_pools: - pool_identities = cognito.pools_identities.get(identity_pool, None) + pool_identities = cognito.pools_identities.get(identity_pool, []) identities.extend( [pi["IdentityId"] for pi in pool_identities.get("Identities", [])] ) diff --git a/moto/logs/models.py b/moto/logs/models.py index b2c0bef93..e12d2de09 100644 --- a/moto/logs/models.py +++ b/moto/logs/models.py @@ -1271,13 +1271,16 @@ class LogsBackend(BaseBackend): self.export_tasks[task_id].status["message"] = "Task is completed" return task_id - def describe_export_tasks(self, taskId: str = "") -> Tuple[List[ExportTask], str]: - if taskId: - if taskId not in self.export_tasks: + def describe_export_tasks(self, task_id: str) -> List[ExportTask]: + """ + Pagination is not yet implemented + """ + if task_id: + if task_id not in self.export_tasks: raise ResourceNotFoundException() - return [self.export_tasks[taskId]], "" + return [self.export_tasks[task_id]] else: - return list(self.export_tasks.values()), "" + return list(self.export_tasks.values()) def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]: return self.tagger.get_tag_dict_for_resource(resource_arn) diff --git a/moto/logs/responses.py b/moto/logs/responses.py index 3c2f7094b..503d315c0 100644 --- a/moto/logs/responses.py +++ b/moto/logs/responses.py @@ -446,10 +446,8 @@ class LogsResponse(BaseResponse): def describe_export_tasks(self) -> str: task_id = self._get_param("taskId") - tasks, next_token = self.logs_backend.describe_export_tasks(taskId=task_id) - return json.dumps( - {"exportTasks": [task.to_json() for task in tasks], "nextToken": next_token} - ) + tasks = self.logs_backend.describe_export_tasks(task_id=task_id) + return json.dumps({"exportTasks": [t.to_json() for t in tasks]}) def list_tags_for_resource(self) -> str: resource_arn = self._get_param("resourceArn") diff --git a/moto/redshift/models.py b/moto/redshift/models.py index 66031dc4a..624429729 100644 --- a/moto/redshift/models.py +++ b/moto/redshift/models.py @@ -8,6 +8,7 @@ from dateutil.tz import tzutc from moto.core import BaseBackend, BackendDict, BaseModel, CloudFormationModel from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.ec2 import ec2_backends +from moto.ec2.models.security_groups import SecurityGroup as EC2SecurityGroup from moto.moto_api._internal import mock_random from .exceptions import ( ClusterAlreadyExistsFaultError, @@ -242,7 +243,7 @@ class Cluster(TaggableResourceMixin, CloudFormationModel): ] @property - def vpc_security_groups(self) -> List["SecurityGroup"]: + def vpc_security_groups(self) -> List["EC2SecurityGroup"]: return [ security_group for security_group in self.redshift_backend.ec2_backend.describe_security_groups() diff --git a/moto/route53/models.py b/moto/route53/models.py index a6bbeaeec..f3d6bf612 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -230,9 +230,8 @@ class RecordSet(CloudFormationModel): zone_name = properties.get("HostedZoneName") backend = route53_backends[account_id]["global"] - if zone_name: - hosted_zone = backend.get_hosted_zone_by_name(zone_name) - else: + hosted_zone = backend.get_hosted_zone_by_name(zone_name) if zone_name else None + if hosted_zone is None: hosted_zone = backend.get_hosted_zone(properties["HostedZoneId"]) record_set = hosted_zone.add_rrset(properties) return record_set @@ -267,9 +266,8 @@ class RecordSet(CloudFormationModel): zone_name = properties.get("HostedZoneName") backend = route53_backends[account_id]["global"] - if zone_name: - hosted_zone = backend.get_hosted_zone_by_name(zone_name) - else: + hosted_zone = backend.get_hosted_zone_by_name(zone_name) if zone_name else None + if hosted_zone is None: hosted_zone = backend.get_hosted_zone(properties["HostedZoneId"]) try: @@ -286,7 +284,11 @@ class RecordSet(CloudFormationModel): ) -> None: """Not exposed as part of the Route 53 API - used for CloudFormation""" backend = route53_backends[account_id]["global"] - hosted_zone = backend.get_hosted_zone_by_name(self.hosted_zone_name) + hosted_zone = ( + backend.get_hosted_zone_by_name(self.hosted_zone_name) + if self.hosted_zone_name + else None + ) if not hosted_zone: hosted_zone = backend.get_hosted_zone(self.hosted_zone_id) hosted_zone.delete_rrset({"Name": self.name, "Type": self.type_}) @@ -460,9 +462,8 @@ class RecordSetGroup(CloudFormationModel): zone_name = properties.get("HostedZoneName") backend = route53_backends[account_id]["global"] - if zone_name: - hosted_zone = backend.get_hosted_zone_by_name(zone_name) - else: + hosted_zone = backend.get_hosted_zone_by_name(zone_name) if zone_name else None + if hosted_zone is None: hosted_zone = backend.get_hosted_zone(properties["HostedZoneId"]) record_sets = properties["RecordSets"] for record_set in record_sets: diff --git a/moto/ssm/models.py b/moto/ssm/models.py index be101c8a4..a650bf425 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -41,6 +41,7 @@ from .exceptions import ( InvalidPermissionType, InvalidResourceId, InvalidResourceType, + ParameterAlreadyExists, ) @@ -211,6 +212,8 @@ class Parameter(CloudFormationModel): tags: Optional[List[Dict[str, str]]] = None, labels: Optional[List[str]] = None, source_result: Optional[str] = None, + tier: Optional[str] = None, + policies: Optional[str] = None, ): self.account_id = account_id self.name = name @@ -224,6 +227,8 @@ class Parameter(CloudFormationModel): self.tags = tags or [] self.labels = labels or [] self.source_result = source_result + self.tier = tier + self.policies = policies if self.parameter_type == "SecureString": if not self.keyid: @@ -255,7 +260,17 @@ class Parameter(CloudFormationModel): "Version": self.version, "LastModifiedDate": round(self.last_modified_date, 3), "DataType": self.data_type, + "Tier": self.tier, } + if self.policies: + try: + policy_list = json.loads(self.policies) + r["Policies"] = [ + {"PolicyText": p, "PolicyType": p, "PolicyStatus": "Finished"} + for p in policy_list + ] + except json.JSONDecodeError: + pass if self.source_result: r["SourceResult"] = self.source_result @@ -316,10 +331,10 @@ class Parameter(CloudFormationModel): "overwrite": properties.get("Overwrite", False), "tags": properties.get("Tags", None), "data_type": properties.get("DataType", "text"), + "tier": properties.get("Tier"), + "policies": properties.get("Policies"), } - ssm_backend.put_parameter(**parameter_args) - parameter = ssm_backend.get_parameter(properties.get("Name")) - return parameter + return ssm_backend.put_parameter(**parameter_args) @classmethod def update_from_cloudformation_json( # type: ignore[misc] @@ -442,7 +457,6 @@ class Documents(BaseModel): version_name: Optional[str] = None, strict: bool = True, ) -> "Document": - if document_version == "$LATEST": ssm_document: Optional["Document"] = self.get_latest_version() elif version_name and document_version: @@ -547,7 +561,6 @@ class Documents(BaseModel): self.permissions.pop(account_id, None) def describe_permissions(self) -> Dict[str, Any]: - permissions_ordered_by_date = sorted( self.permissions.values(), key=lambda p: p.created_at ) @@ -678,7 +691,6 @@ class Command(BaseModel): targets: Optional[List[Dict[str, Any]]] = None, backend_region: str = "us-east-1", ): - if instance_ids is None: instance_ids = [] @@ -1336,7 +1348,6 @@ class SimpleSystemManagerBackend(BaseBackend): def get_document( self, name: str, document_version: str, version_name: str, document_format: str ) -> Dict[str, Any]: - documents = self._get_documents(name) ssm_document = documents.find(document_version, version_name) @@ -1488,7 +1499,6 @@ class SimpleSystemManagerBackend(BaseBackend): shared_document_version: str, permission_type: str, ) -> None: - account_id_regex = re.compile(r"^(all|[0-9]{12})$", re.IGNORECASE) version_regex = re.compile(r"^([$]LATEST|[$]DEFAULT|[$]ALL)$") @@ -1810,7 +1820,6 @@ class SimpleSystemManagerBackend(BaseBackend): def get_parameter_history( self, name: str, next_token: Optional[str], max_results: int = 50 ) -> Tuple[Optional[List[Parameter]], Optional[str]]: - if max_results > PARAMETER_HISTORY_MAX_RESULTS: raise ValidationException( "1 validation error detected: " @@ -2034,7 +2043,9 @@ class SimpleSystemManagerBackend(BaseBackend): overwrite: bool, tags: List[Dict[str, str]], data_type: str, - ) -> Optional[int]: + tier: Optional[str], + policies: Optional[str], + ) -> Parameter: if not value: raise ValidationException( "1 validation error detected: Value '' at 'value' failed to satisfy" @@ -2097,7 +2108,7 @@ class SimpleSystemManagerBackend(BaseBackend): version = previous_parameter.version + 1 if not overwrite: - return None + raise ParameterAlreadyExists # overwriting a parameter, Type is not included in boto3 call if not parameter_type and overwrite: parameter_type = previous_parameter.parameter_type @@ -2123,29 +2134,32 @@ class SimpleSystemManagerBackend(BaseBackend): data_type = ( data_type if data_type is not None else previous_parameter.data_type ) + tier = tier if tier is not None else previous_parameter.tier + policies = policies if policies is not None else previous_parameter.policies last_modified_date = time.time() - self._parameters[name].append( - Parameter( - account_id=self.account_id, - name=name, - value=value, - parameter_type=parameter_type, - description=description, - allowed_pattern=allowed_pattern, - keyid=keyid, - last_modified_date=last_modified_date, - version=version, - tags=tags or [], - data_type=data_type, - ) + new_param = Parameter( + account_id=self.account_id, + name=name, + value=value, + parameter_type=parameter_type, + description=description, + allowed_pattern=allowed_pattern, + keyid=keyid, + last_modified_date=last_modified_date, + version=version, + tags=tags or [], + data_type=data_type, + tier=tier, + policies=policies, ) + self._parameters[name].append(new_param) if tags: tag_dict = {t["Key"]: t["Value"] for t in tags} self.add_tags_to_resource("Parameter", name, tag_dict) - return version + return new_param def add_tags_to_resource( self, resource_type: str, resource_id: str, tags: Dict[str, str] @@ -2460,7 +2474,6 @@ class SimpleSystemManagerBackend(BaseBackend): cutoff_behavior: Optional[str], alarm_configurations: Optional[Dict[str, Any]], ) -> str: - window = self.get_maintenance_window(window_id) task = FakeMaintenanceWindowTask( window_id, diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index aec651564..2d565a2c2 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -1,9 +1,8 @@ import json from typing import Any, Dict, Tuple, Union -import warnings from moto.core.responses import BaseResponse -from .exceptions import ValidationException, ParameterAlreadyExists +from .exceptions import ValidationException from .models import ssm_backends, SimpleSystemManagerBackend @@ -273,20 +272,10 @@ class SimpleSystemManagerResponse(BaseResponse): overwrite = self._get_param("Overwrite", False) tags = self._get_param("Tags") data_type = self._get_param("DataType", "text") - - # To be implemented arguments of put_parameter tier = self._get_param("Tier") - if tier is not None: - warnings.warn( - "Tier configuration option is not yet implemented. Discarding." - ) policies = self._get_param("Policies") - if policies is not None: - warnings.warn( - "Policies configuration option is not yet implemented. Discarding." - ) - result = self.ssm_backend.put_parameter( + param = self.ssm_backend.put_parameter( name, description, value, @@ -296,12 +285,11 @@ class SimpleSystemManagerResponse(BaseResponse): overwrite, tags, data_type, + tier=tier, + policies=policies, ) - if result is None: - raise ParameterAlreadyExists - - response = {"Version": result} + response = {"Version": param.version} return json.dumps(response) def get_parameter_history(self) -> Union[str, Tuple[str, Dict[str, int]]]: diff --git a/tests/test_ecs/test_ecs_cloudformation.py b/tests/test_ecs/test_ecs_cloudformation.py index 72556ca85..2f83e470b 100644 --- a/tests/test_ecs/test_ecs_cloudformation.py +++ b/tests/test_ecs/test_ecs_cloudformation.py @@ -177,6 +177,7 @@ def test_update_service_through_cloudformation_should_trigger_replacement(): cfn_conn = boto3.client("cloudformation", region_name="us-west-1") cfn_conn.create_stack(StackName="test_stack", TemplateBody=template_json1) template2 = deepcopy(template1) + template2["Resources"]["testCluster"]["Properties"]["ClusterName"] = "updated name" template2["Resources"]["testService"]["Properties"]["DesiredCount"] = 5 template2_json = json.dumps(template2) cfn_conn.update_stack(StackName="test_stack", TemplateBody=template2_json) diff --git a/tests/test_elb/test_elb_cloudformation.py b/tests/test_elb/test_elb_cloudformation.py index 29a6311bf..d38d3ff67 100644 --- a/tests/test_elb/test_elb_cloudformation.py +++ b/tests/test_elb/test_elb_cloudformation.py @@ -112,7 +112,19 @@ def test_stack_elb_integration_with_update(): "Protocol": "HTTP", } ], - "Policies": {"Ref": "AWS::NoValue"}, + "Policies": [ + { + "PolicyName": "My-SSLNegotiation-Policy", + "PolicyType": "SSLNegotiationPolicyType", + "InstancePorts": ["80"], + "Attributes": [ + { + "Name": "Reference-Security-Policy", + "Value": "ELBSecurityPolicy-TLS-1-2-2017-01", + } + ], + } + ], }, } }, @@ -127,6 +139,7 @@ def test_stack_elb_integration_with_update(): elb = boto3.client("elb", region_name="us-west-1") load_balancer = elb.describe_load_balancers()["LoadBalancerDescriptions"][0] assert load_balancer["AvailabilityZones"] == ["us-west-1a"] + assert load_balancer["Policies"]["OtherPolicies"] == ["My-SSLNegotiation-Policy"] # when elb_template["Resources"]["MyELB"]["Properties"]["AvailabilityZones"] = [ diff --git a/tests/test_iam/test_iam_access_integration.py b/tests/test_iam/test_iam_access_integration.py index b977fb8aa..3c4f6af09 100644 --- a/tests/test_iam/test_iam_access_integration.py +++ b/tests/test_iam/test_iam_access_integration.py @@ -1,9 +1,12 @@ import datetime import boto3 +import csv from moto import mock_ec2, mock_iam, mock_sts, settings from moto.iam.models import iam_backends, IAMBackend +from dateutil.parser import parse from tests import DEFAULT_ACCOUNT_ID +from unittest import SkipTest @mock_ec2 @@ -60,3 +63,35 @@ def test_mark_role_as_last_used(): if not settings.TEST_SERVER_MODE: iam: IAMBackend = iam_backends[DEFAULT_ACCOUNT_ID]["global"] assert iam.get_role(role_name).last_used is not None + + +@mock_ec2 +@mock_iam +def test_get_credential_report_content__set_last_used_automatically(): + if not settings.TEST_DECORATOR_MODE: + raise SkipTest("No point testing this in ServerMode") + # Ensure LAST_USED field is set + c_iam = boto3.client("iam", region_name="us-east-1") + c_iam.create_user(Path="my/path", UserName="fakeUser") + key = c_iam.create_access_key(UserName="fakeUser") + + c_ec2 = boto3.client( + "ec2", + region_name="us-east-2", + aws_access_key_id=key["AccessKey"]["AccessKeyId"], + aws_secret_access_key=key["AccessKey"]["SecretAccessKey"], + ) + c_ec2.describe_instances() + + # VERIFY last_used can be retrieved + conn = boto3.client("iam", region_name="us-east-1") + + result = conn.generate_credential_report() + while result["State"] != "COMPLETE": + result = conn.generate_credential_report() + result = conn.get_credential_report() + report = result["Content"].decode("utf-8") + report_dict = csv.DictReader(report.split("\n")) + user = next(report_dict) + + assert parse(user["access_key_1_last_used_date"]) diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index d2daa02cf..fc8a4af17 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -2,15 +2,13 @@ import datetime import re import string import uuid -from unittest.mock import patch, Mock -from unittest import SkipTest import boto3 import botocore.exceptions from botocore.exceptions import ClientError import pytest -from moto import mock_ec2, mock_ssm, settings +from moto import mock_ec2, mock_ssm from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID from moto.ssm.models import PARAMETER_VERSION_LIMIT, PARAMETER_HISTORY_MAX_RESULTS from tests import EXAMPLE_AMI_ID @@ -304,55 +302,6 @@ def test_put_parameter(name): ) -@mock_ssm -def test_put_parameter_unimplemented_parameters(): - """ - Test to ensure coverage of unimplemented parameters. Remove for appropriate tests - once implemented - """ - if settings.TEST_SERVER_MODE: - raise SkipTest("Can't test for warning logs in server mode") - - mock_warn = Mock() - with patch("warnings.warn", mock_warn): - # Ensure that the ssm parameters are still working with the Mock - client = boto3.client("ssm", region_name="us-east-1") - name = "my-param" - response = client.put_parameter( - Name=name, - Description="A test parameter", - Value="value", - Type="String", - Tier="Advanced", - Policies="No way fam", - ) - - assert response["Version"] == 1 - - response = client.get_parameters(Names=[name], WithDecryption=False) - - assert len(response["Parameters"]) == 1 - assert response["Parameters"][0]["Name"] == name - assert response["Parameters"][0]["Value"] == "value" - assert response["Parameters"][0]["Type"] == "String" - assert response["Parameters"][0]["Version"] == 1 - assert response["Parameters"][0]["DataType"] == "text" - assert isinstance( - response["Parameters"][0]["LastModifiedDate"], datetime.datetime - ) - assert response["Parameters"][0]["ARN"] == ( - f"arn:aws:ssm:us-east-1:{ACCOUNT_ID}:parameter/{name}" - ) - - # We got the argument warnings - mock_warn.assert_any_call( - "Tier configuration option is not yet implemented. Discarding." - ) - mock_warn.assert_any_call( - "Policies configuration option is not yet implemented. Discarding." - ) - - @pytest.mark.parametrize("name", ["test", "my-cool-parameter"]) @mock_ssm def test_put_parameter_overwrite_preserves_metadata(name): @@ -367,12 +316,11 @@ def test_put_parameter_overwrite_preserves_metadata(name): Description=test_description, Value="value", Type="String", - Tags=[ - {"Key": test_tag_key, "Value": test_tag_value}, - ], + Tags=[{"Key": test_tag_key, "Value": test_tag_value}], AllowedPattern=test_pattern, KeyId=test_key_id, - # TODO: add tier and policies support + Tier="Standard", + Policies='["Expiration"]', ) assert response["Version"] == 1 @@ -430,7 +378,6 @@ def test_put_parameter_overwrite_preserves_metadata(name): assert response["Parameters"][0]["ARN"] == ( f"arn:aws:ssm:us-east-1:{ACCOUNT_ID}:parameter/{name}" ) - initial_modification_date = response["Parameters"][0]["LastModifiedDate"] # Verify that tags are unchanged response = client.list_tags_for_resource(ResourceType="Parameter", ResourceId=name) @@ -439,20 +386,42 @@ def test_put_parameter_overwrite_preserves_metadata(name): assert response["TagList"][0]["Key"] == test_tag_key assert response["TagList"][0]["Value"] == test_tag_value - # Verify description is unchanged + # Verify description/tier/policies is unchanged response = client.describe_parameters( - ParameterFilters=[ - { - "Key": "Name", - "Option": "Equals", - "Values": [name], - }, - ] + ParameterFilters=[{"Key": "Name", "Option": "Equals", "Values": [name]}] ) assert len(response["Parameters"]) == 1 assert response["Parameters"][0]["Description"] == test_description assert response["Parameters"][0]["AllowedPattern"] == test_pattern assert response["Parameters"][0]["KeyId"] == test_key_id + assert response["Parameters"][0]["Tier"] == "Standard" + assert response["Parameters"][0]["Policies"] == [ + { + "PolicyStatus": "Finished", + "PolicyText": "Expiration", + "PolicyType": "Expiration", + } + ] + + +@mock_ssm +def test_put_parameter_with_invalid_policy(): + name = "some_param" + test_description = "A test parameter" + client = boto3.client("ssm", region_name="us-east-1") + client.put_parameter( + Name=name, + Description=test_description, + Value="value", + Type="String", + Policies="invalid json", + ) + + # Verify that an invalid policy does not break anything + param = client.describe_parameters( + ParameterFilters=[{"Key": "Name", "Option": "Equals", "Values": [name]}] + )["Parameters"][0] + assert "Policies" not in param @mock_ssm