diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py index f68e47029..0d2fdee3b 100644 --- a/moto/ssm/exceptions.py +++ b/moto/ssm/exceptions.py @@ -115,3 +115,12 @@ class DuplicateDocumentContent(JsonRESTError): super(DuplicateDocumentContent, self).__init__( "DuplicateDocumentContent", message ) + + +class ParameterMaxVersionLimitExceeded(JsonRESTError): + code = 400 + + def __init__(self, message): + super(ParameterMaxVersionLimitExceeded, self).__init__( + "ParameterMaxVersionLimitExceeded", message + ) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 72e9b21e3..74f60e58a 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -32,9 +32,13 @@ from .exceptions import ( InvalidDocumentVersion, DuplicateDocumentVersionName, DuplicateDocumentContent, + ParameterMaxVersionLimitExceeded, ) +PARAMETER_VERSION_LIMIT = 100 + + class Parameter(BaseModel): def __init__( self, @@ -1281,6 +1285,18 @@ class SimpleSystemManagerBackend(BaseBackend): parameter.labels.remove(label) return [invalid_labels, version] + def _check_for_parameter_version_limit_exception(self, name): + # https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-versions.html + parameter_versions = self._parameters[name] + oldest_parameter = parameter_versions[0] + if oldest_parameter.labels: + raise ParameterMaxVersionLimitExceeded( + "You attempted to create a new version of %s by calling the PutParameter API " + "with the overwrite flag. Version %d, the oldest version, can't be deleted " + "because it has a label associated with it. Move the label to another version " + "of the parameter, and try again." % (name, oldest_parameter.version) + ) + def put_parameter( self, name, description, value, type, allowed_pattern, keyid, overwrite, tags, ): @@ -1317,6 +1333,10 @@ class SimpleSystemManagerBackend(BaseBackend): if not overwrite: return + if len(previous_parameter_versions) >= PARAMETER_VERSION_LIMIT: + self._check_for_parameter_version_limit_exception(name) + previous_parameter_versions.pop(0) + last_modified_date = time.time() self._parameters[name].append( Parameter( diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 18b48456f..3301ed32c 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -12,6 +12,7 @@ from botocore.exceptions import ClientError import pytest from moto import mock_ec2, mock_ssm +from moto.ssm.models import PARAMETER_VERSION_LIMIT from tests import EXAMPLE_AMI_ID @@ -1755,3 +1756,56 @@ def test_get_command_invocations_by_instance_tag(): for instance_id in instance_ids: resp = ssm.get_command_invocation(CommandId=command_id, InstanceId=instance_id) resp["Status"].should.equal("Success") + + +@mock_ssm +def test_parameter_version_limit(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + for i in range(PARAMETER_VERSION_LIMIT + 1): + client.put_parameter( + Name=parameter_name, + Value="value-%d" % (i + 1), + Type="String", + Overwrite=True, + ) + + paginator = client.get_paginator("get_parameter_history") + page_iterator = paginator.paginate(Name=parameter_name) + parameter_history = list( + item for page in page_iterator for item in page["Parameters"] + ) + + len(parameter_history).should.equal(PARAMETER_VERSION_LIMIT) + parameter_history[0]["Value"].should.equal("value-2") + latest_version_index = PARAMETER_VERSION_LIMIT - 1 + latest_version_value = "value-%d" % (PARAMETER_VERSION_LIMIT + 1) + parameter_history[latest_version_index]["Value"].should.equal(latest_version_value) + + +@mock_ssm +def test_parameter_overwrite_fails_when_limit_reached_and_oldest_version_has_label(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + for i in range(PARAMETER_VERSION_LIMIT): + client.put_parameter( + Name=parameter_name, + Value="value-%d" % (i + 1), + Type="String", + Overwrite=True, + ) + client.label_parameter_version( + Name=parameter_name, ParameterVersion=1, Labels=["test-label"] + ) + + with pytest.raises(ClientError) as ex: + client.put_parameter( + Name=parameter_name, Value="new-value", Type="String", Overwrite=True, + ) + error = ex.value.response["Error"] + error["Code"].should.equal("ParameterMaxVersionLimitExceeded") + error["Message"].should.contain(parameter_name) + error["Message"].should.contain("Version 1") + error["Message"].should.match( + r"the oldest version, can't be deleted because it has a label associated with it. Move the label to another version of the parameter, and try again." + )