Repository: getmoto/moto

Issue:
SSM Parameter Store Should Only Support 100 Versions
Per https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-versions.html - 

AWS SSM Parameter Store only supports 100 versions. After this point, the lowest version is deleted every time a new version is created. 

This functionality is not currently implemented in SimpleSystemManagerBackend. 

To reproduce, simply add 101 versions to a parameter and then fetch the history. Only the most recent 100 should be returned, instead all 101 are returned. 



Discussion:
Thanks for raising this @bigahen - marking this as an enhancement.

Some relevant information from that link, for whoever ends up implementing this:

>
> An exception to this is when there are already 100 parameter versions in history, and a parameter label is assigned to the oldest version of a parameter. In this case, that version isn't removed from history, and the request to create a new parameter version fails. This safeguard is to prevent parameter versions with mission critical labels assigned to them from being deleted. To continue creating new parameters, first move the label from the oldest version of the parameter to a newer one for use in your operations.


Resolving patch:
diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py
--- a/moto/ssm/exceptions.py
+++ b/moto/ssm/exceptions.py
@@ -115,3 +115,12 @@ def __init__(self, message):
         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
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -32,9 +32,13 @@
     InvalidDocumentVersion,
     DuplicateDocumentVersionName,
     DuplicateDocumentContent,
+    ParameterMaxVersionLimitExceeded,
 )
 
 
+PARAMETER_VERSION_LIMIT = 100
+
+
 class Parameter(BaseModel):
     def __init__(
         self,
@@ -1281,6 +1285,18 @@ def label_parameter_version(self, name, version, labels):
                         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 @@ def put_parameter(
             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(


Test patch:
diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py
--- a/tests/test_ssm/test_ssm_boto3.py
+++ b/tests/test_ssm/test_ssm_boto3.py
@@ -12,6 +12,7 @@
 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."
+    )