diff --git a/moto/ssm/models.py b/moto/ssm/models.py index fe6c64ee8..5c2a5cce7 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -37,6 +37,7 @@ from .exceptions import ( PARAMETER_VERSION_LIMIT = 100 +PARAMETER_HISTORY_MAX_RESULTS = 50 class Parameter(BaseModel): @@ -1071,7 +1072,7 @@ class SimpleSystemManagerBackend(BaseBackend): return result def get_parameters(self, names, with_decryption): - result = [] + result = {} if len(names) > 10: raise ValidationException( @@ -1082,9 +1083,15 @@ class SimpleSystemManagerBackend(BaseBackend): ) ) - for name in names: - if name in self._parameters: - result.append(self.get_parameter(name, with_decryption)) + for name in set(names): + if name.split(":")[0] in self._parameters: + try: + param = self.get_parameter(name, with_decryption) + + if param is not None: + result[name] = param + except ParameterVersionNotFound: + pass return result def get_parameters_by_path( @@ -1129,10 +1136,36 @@ class SimpleSystemManagerBackend(BaseBackend): next_token = None return values, next_token - def get_parameter_history(self, name, with_decryption): + def get_parameter_history(self, name, with_decryption, next_token, max_results=50): + + if max_results > PARAMETER_HISTORY_MAX_RESULTS: + raise ValidationException( + "1 validation error detected: " + "Value '{}' at 'maxResults' failed to satisfy constraint: " + "Member must have value less than or equal to {}.".format( + max_results, PARAMETER_HISTORY_MAX_RESULTS + ) + ) + if name in self._parameters: - return self._parameters[name] - return None + history = self._parameters[name] + return self._get_history_nexttoken(history, next_token, max_results) + + return None, None + + def _get_history_nexttoken(self, history, next_token, max_results): + if next_token is None: + next_token = 0 + next_token = int(next_token) + max_results = int(max_results) + history_to_return = history[next_token : next_token + max_results] + if ( + len(history_to_return) == max_results + and len(history) > next_token + max_results + ): + new_next_token = next_token + max_results + return history_to_return, str(new_next_token) + return history_to_return, None def _match_filters(self, parameter, filters=None): """Return True if the given parameter matches all the filters""" diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index be298d5bc..d99140c3a 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -178,13 +178,13 @@ class SimpleSystemManagerResponse(BaseResponse): response = {"Parameters": [], "InvalidParameters": []} - for parameter in result: + for name, parameter in result.items(): param_data = parameter.response_object(with_decryption, self.region) response["Parameters"].append(param_data) - param_names = [param.name for param in result] + valid_param_names = [name for name, parameter in result.items()] for name in names: - if name not in param_names: + if name not in valid_param_names: response["InvalidParameters"].append(name) return json.dumps(response) @@ -266,8 +266,12 @@ class SimpleSystemManagerResponse(BaseResponse): def get_parameter_history(self): name = self._get_param("Name") with_decryption = self._get_param("WithDecryption") + next_token = self._get_param("NextToken") + max_results = self._get_param("MaxResults", 50) - result = self.ssm_backend.get_parameter_history(name, with_decryption) + result, new_next_token = self.ssm_backend.get_parameter_history( + name, with_decryption, next_token, max_results + ) if result is None: error = { @@ -283,6 +287,9 @@ class SimpleSystemManagerResponse(BaseResponse): ) response["Parameters"].append(param_data) + if new_next_token is not None: + response["NextToken"] = new_next_token + return json.dumps(response) def label_parameter_version(self): diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index ddf033cfc..73b80c893 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -12,7 +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 moto.ssm.models import PARAMETER_VERSION_LIMIT, PARAMETER_HISTORY_MAX_RESULTS from tests import EXAMPLE_AMI_ID @@ -1816,3 +1816,145 @@ def test_parameter_overwrite_fails_when_limit_reached_and_oldest_version_has_lab 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." ) + + +@mock_ssm +def test_get_parameters_includes_invalid_parameter_when_requesting_invalid_version(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + versions_to_create = 5 + + for i in range(versions_to_create): + client.put_parameter( + Name=parameter_name, + Value="value-%d" % (i + 1), + Type="String", + Overwrite=True, + ) + + response = client.get_parameters( + Names=[ + "test-param:%d" % (versions_to_create + 1), + "test-param:%d" % (versions_to_create - 1), + ] + ) + + len(response["InvalidParameters"]).should.equal(1) + response["InvalidParameters"][0].should.equal( + "test-param:%d" % (versions_to_create + 1) + ) + + len(response["Parameters"]).should.equal(1) + response["Parameters"][0]["Name"].should.equal("test-param") + response["Parameters"][0]["Value"].should.equal("value-4") + response["Parameters"][0]["Type"].should.equal("String") + + +@mock_ssm +def test_get_parameters_includes_invalid_parameter_when_requesting_invalid_label(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + versions_to_create = 5 + + for i in range(versions_to_create): + 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"] + ) + + response = client.get_parameters( + Names=[ + "test-param:test-label", + "test-param:invalid-label", + "test-param", + "test-param:2", + ] + ) + + len(response["InvalidParameters"]).should.equal(1) + response["InvalidParameters"][0].should.equal("test-param:invalid-label") + + len(response["Parameters"]).should.equal(3) + + +@mock_ssm +def test_get_parameters_should_only_return_unique_requests(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + + client.put_parameter(Name=parameter_name, Value="value", Type="String") + + response = client.get_parameters(Names=["test-param", "test-param"]) + + len(response["Parameters"]).should.equal(1) + + +@mock_ssm +def test_get_parameter_history_should_throw_exception_when_MaxResults_is_too_large(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + + for _ in range(100): + client.put_parameter( + Name=parameter_name, Value="value", Type="String", Overwrite=True + ) + + with pytest.raises(ClientError) as ex: + client.get_parameter_history( + Name=parameter_name, MaxResults=PARAMETER_HISTORY_MAX_RESULTS + 1 + ) + + error = ex.value.response["Error"] + error["Code"].should.equal("ValidationException") + error["Message"].should.equal( + "1 validation error detected: " + "Value '{}' at 'maxResults' failed to satisfy constraint: " + "Member must have value less than or equal to 50.".format( + PARAMETER_HISTORY_MAX_RESULTS + 1 + ) + ) + + +@mock_ssm +def test_get_parameter_history_NextTokenImplementation(): + client = boto3.client("ssm", region_name="us-east-1") + parameter_name = "test-param" + + for _ in range(100): + client.put_parameter( + Name=parameter_name, Value="value", Type="String", Overwrite=True + ) + + response = client.get_parameter_history( + Name=parameter_name, MaxResults=PARAMETER_HISTORY_MAX_RESULTS + ) # fetch first 50 + + param_history = response["Parameters"] + next_token = response.get("NextToken", None) + + while next_token is not None: + response = client.get_parameter_history( + Name=parameter_name, MaxResults=7, NextToken=next_token + ) # fetch small amounts to test MaxResults can change + param_history.extend(response["Parameters"]) + next_token = response.get("NextToken", None) + + len(param_history).should.equal(100) + + +@mock_ssm +def test_get_parameter_history_exception_when_requesting_invalid_parameter(): + client = boto3.client("ssm", region_name="us-east-1") + + with pytest.raises(ClientError) as ex: + client.get_parameter_history(Name="invalid_parameter_name") + + error = ex.value.response["Error"] + error["Code"].should.equal("ParameterNotFound") + error["Message"].should.equal("Parameter invalid_parameter_name not found.")