From b4fb4b3b5d10cd13b1162324353f6d5efc5e2bb0 Mon Sep 17 00:00:00 2001 From: William Richard Date: Mon, 4 Nov 2019 12:43:37 -0500 Subject: [PATCH 1/4] Store all parameter versions, not just the latest version --- moto/ssm/models.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 0e0f8d353..0f58a5ad5 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -259,7 +259,10 @@ class Command(BaseModel): class SimpleSystemManagerBackend(BaseBackend): def __init__(self): - self._parameters = {} + # each value is a list of all of the versions for a parameter + # to get the current value, grab the last item of the list + self._parameters = defaultdict(list) + self._resource_tags = defaultdict(lambda: defaultdict(dict)) self._commands = [] self._errors = [] @@ -294,8 +297,8 @@ class SimpleSystemManagerBackend(BaseBackend): self._validate_parameter_filters(parameter_filters, by_path=False) result = [] - for param in self._parameters: - ssm_parameter = self._parameters[param] + for param_name in self._parameters: + ssm_parameter = self.get_parameter(param_name, False) if not self._match_filters(ssm_parameter, parameter_filters): continue @@ -504,7 +507,7 @@ class SimpleSystemManagerBackend(BaseBackend): result = [] for name in names: if name in self._parameters: - result.append(self._parameters[name]) + result.append(self.get_parameter(name, with_decryption)) return result def get_parameters_by_path(self, path, with_decryption, recursive, filters=None): @@ -513,14 +516,16 @@ class SimpleSystemManagerBackend(BaseBackend): # path could be with or without a trailing /. we handle this # difference here. path = path.rstrip("/") + "/" - for param in self._parameters: - if path != "/" and not param.startswith(path): + for param_name in self._parameters: + if path != "/" and not param_name.startswith(path): continue - if "/" in param[len(path) + 1 :] and not recursive: + if "/" in param_name[len(path) + 1 :] and not recursive: continue - if not self._match_filters(self._parameters[param], filters): + if not self._match_filters(self.get_parameter(param_name, with_decryption), filters): continue - result.append(self._parameters[param]) + result.append(self.get_parameter(param_name, with_decryption)) + + return result return result @@ -579,23 +584,25 @@ class SimpleSystemManagerBackend(BaseBackend): def get_parameter(self, name, with_decryption): if name in self._parameters: - return self._parameters[name] + return self._parameters[name][-1] return None def put_parameter( self, name, description, value, type, allowed_pattern, keyid, overwrite ): - previous_parameter = self._parameters.get(name) - version = 1 - - if previous_parameter: + previous_parameter_versions = self._parameters[name] + if len(previous_parameter_versions) == 0: + previous_parameter = None + version = 1 + else: + previous_parameter = previous_parameter_versions[-1] version = previous_parameter.version + 1 if not overwrite: return last_modified_date = time.time() - self._parameters[name] = Parameter( + self._parameters[name].append(Parameter( name, value, type, @@ -604,7 +611,7 @@ class SimpleSystemManagerBackend(BaseBackend): keyid, last_modified_date, version, - ) + )) return version def add_tags_to_resource(self, resource_type, resource_id, tags): From aeb7974549fc4443be44fe62158996670a339e3b Mon Sep 17 00:00:00 2001 From: William Richard Date: Mon, 4 Nov 2019 12:49:09 -0500 Subject: [PATCH 2/4] Add get_parameter_history implementation and tests --- moto/ssm/models.py | 5 ++- moto/ssm/responses.py | 13 ++++++++ tests/test_ssm/test_ssm_boto3.py | 54 ++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 0f58a5ad5..e4f1727be 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -527,7 +527,10 @@ class SimpleSystemManagerBackend(BaseBackend): return result - return result + def get_parameter_history(self, name, with_decryption): + if name in self._parameters: + return self._parameters[name] + 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 0bb034428..2711e5c6e 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -139,6 +139,19 @@ class SimpleSystemManagerResponse(BaseResponse): response = {"Version": result} return json.dumps(response) + def get_parameter_history(self): + name = self._get_param('Name') + with_decryption = self._get_param("WithDecryption") + + result = self.ssm_backend.get_parameter_history(name, with_decryption) + + response = {"Parameters": []} + for parameter_version in result: + param_data = parameter_version.describe_response_object(decrypt=with_decryption) + response['Parameters'].append(param_data) + + return json.dumps(response) + def add_tags_to_resource(self): resource_id = self._get_param("ResourceId") resource_type = self._get_param("ResourceType") diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index d50ceb528..0c8874c7b 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -813,6 +813,60 @@ def test_put_parameter_secure_custom_kms(): response["Parameters"][0]["Value"].should.equal("value") response["Parameters"][0]["Type"].should.equal("SecureString") +@mock_ssm +def test_get_parameter_history(): + client = boto3.client("ssm", region_name="us-east-1") + + test_parameter_name = "test" + + for i in range(3): + client.put_parameter( + Name=test_parameter_name, Description="A test parameter version %d" % i, Value="value-%d" % i, Type="String", + Overwrite=True + ) + + response = client.get_parameter_history(Name=test_parameter_name) + parameters_response = response['Parameters'] + + for index, param in enumerate(parameters_response): + param['Name'].should.equal(test_parameter_name) + param['Type'].should.equal('String') + param['Value'].should.equal('value-%d' % index) + param['Version'].should.equal(index + 1) + param['Description'].should.equal("A test parameter version %d" % index) + + len(parameters_response).should.equal(3) + +@mock_ssm +def test_get_parameter_history_with_secure_string(): + client = boto3.client("ssm", region_name="us-east-1") + + test_parameter_name = "test" + + for i in range(3): + client.put_parameter( + Name=test_parameter_name, Description="A test parameter version %d" % i, Value="value-%d" % i, Type="SecureString", + Overwrite=True + ) + + for with_decryption in [True, False]: + response = client.get_parameter_history(Name=test_parameter_name, WithDecryption=with_decryption) + parameters_response = response['Parameters'] + + for index, param in enumerate(parameters_response): + param['Name'].should.equal(test_parameter_name) + param['Type'].should.equal('SecureString') + expected_plaintext_value = 'value-%d' % index + if with_decryption: + param['Value'].should.equal(expected_plaintext_value) + else: + param['Value'].should.equal('kms:alias/aws/ssm:%s' % expected_plaintext_value) + param['Version'].should.equal(index + 1) + param['Description'].should.equal("A test parameter version %d" % index) + + len(parameters_response).should.equal(3) + + @mock_ssm def test_add_remove_list_tags_for_resource(): From 3816eba58feed721a14f96f6014c3160d11b3e0a Mon Sep 17 00:00:00 2001 From: William Richard Date: Mon, 4 Nov 2019 13:04:10 -0500 Subject: [PATCH 3/4] Fix linting --- moto/ssm/models.py | 26 +++++++++------- moto/ssm/responses.py | 8 +++-- tests/test_ssm/test_ssm_boto3.py | 51 +++++++++++++++++++------------- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index e4f1727be..65b4d6743 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -521,7 +521,9 @@ class SimpleSystemManagerBackend(BaseBackend): continue if "/" in param_name[len(path) + 1 :] and not recursive: continue - if not self._match_filters(self.get_parameter(param_name, with_decryption), filters): + if not self._match_filters( + self.get_parameter(param_name, with_decryption), filters + ): continue result.append(self.get_parameter(param_name, with_decryption)) @@ -605,16 +607,18 @@ class SimpleSystemManagerBackend(BaseBackend): return last_modified_date = time.time() - self._parameters[name].append(Parameter( - name, - value, - type, - description, - allowed_pattern, - keyid, - last_modified_date, - version, - )) + self._parameters[name].append( + Parameter( + name, + value, + type, + description, + allowed_pattern, + keyid, + last_modified_date, + version, + ) + ) return version def add_tags_to_resource(self, resource_type, resource_id, tags): diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index 2711e5c6e..236c22005 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -140,15 +140,17 @@ class SimpleSystemManagerResponse(BaseResponse): return json.dumps(response) def get_parameter_history(self): - name = self._get_param('Name') + name = self._get_param("Name") with_decryption = self._get_param("WithDecryption") result = self.ssm_backend.get_parameter_history(name, with_decryption) response = {"Parameters": []} for parameter_version in result: - param_data = parameter_version.describe_response_object(decrypt=with_decryption) - response['Parameters'].append(param_data) + param_data = parameter_version.describe_response_object( + decrypt=with_decryption + ) + response["Parameters"].append(param_data) return json.dumps(response) diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 0c8874c7b..6b521132e 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -813,6 +813,7 @@ def test_put_parameter_secure_custom_kms(): response["Parameters"][0]["Value"].should.equal("value") response["Parameters"][0]["Type"].should.equal("SecureString") + @mock_ssm def test_get_parameter_history(): client = boto3.client("ssm", region_name="us-east-1") @@ -821,22 +822,26 @@ def test_get_parameter_history(): for i in range(3): client.put_parameter( - Name=test_parameter_name, Description="A test parameter version %d" % i, Value="value-%d" % i, Type="String", - Overwrite=True + Name=test_parameter_name, + Description="A test parameter version %d" % i, + Value="value-%d" % i, + Type="String", + Overwrite=True, ) response = client.get_parameter_history(Name=test_parameter_name) - parameters_response = response['Parameters'] + parameters_response = response["Parameters"] for index, param in enumerate(parameters_response): - param['Name'].should.equal(test_parameter_name) - param['Type'].should.equal('String') - param['Value'].should.equal('value-%d' % index) - param['Version'].should.equal(index + 1) - param['Description'].should.equal("A test parameter version %d" % index) + param["Name"].should.equal(test_parameter_name) + param["Type"].should.equal("String") + param["Value"].should.equal("value-%d" % index) + param["Version"].should.equal(index + 1) + param["Description"].should.equal("A test parameter version %d" % index) len(parameters_response).should.equal(3) + @mock_ssm def test_get_parameter_history_with_secure_string(): client = boto3.client("ssm", region_name="us-east-1") @@ -845,29 +850,35 @@ def test_get_parameter_history_with_secure_string(): for i in range(3): client.put_parameter( - Name=test_parameter_name, Description="A test parameter version %d" % i, Value="value-%d" % i, Type="SecureString", - Overwrite=True + Name=test_parameter_name, + Description="A test parameter version %d" % i, + Value="value-%d" % i, + Type="SecureString", + Overwrite=True, ) for with_decryption in [True, False]: - response = client.get_parameter_history(Name=test_parameter_name, WithDecryption=with_decryption) - parameters_response = response['Parameters'] + response = client.get_parameter_history( + Name=test_parameter_name, WithDecryption=with_decryption + ) + parameters_response = response["Parameters"] for index, param in enumerate(parameters_response): - param['Name'].should.equal(test_parameter_name) - param['Type'].should.equal('SecureString') - expected_plaintext_value = 'value-%d' % index + param["Name"].should.equal(test_parameter_name) + param["Type"].should.equal("SecureString") + expected_plaintext_value = "value-%d" % index if with_decryption: - param['Value'].should.equal(expected_plaintext_value) + param["Value"].should.equal(expected_plaintext_value) else: - param['Value'].should.equal('kms:alias/aws/ssm:%s' % expected_plaintext_value) - param['Version'].should.equal(index + 1) - param['Description'].should.equal("A test parameter version %d" % index) + param["Value"].should.equal( + "kms:alias/aws/ssm:%s" % expected_plaintext_value + ) + param["Version"].should.equal(index + 1) + param["Description"].should.equal("A test parameter version %d" % index) len(parameters_response).should.equal(3) - @mock_ssm def test_add_remove_list_tags_for_resource(): client = boto3.client("ssm", region_name="us-east-1") From 715ff0f7afc51ebf2f193eed990e8d9e7f236e2b Mon Sep 17 00:00:00 2001 From: William Richard Date: Mon, 4 Nov 2019 15:30:30 -0500 Subject: [PATCH 4/4] Return a sensible error when the parameter is not found --- moto/ssm/responses.py | 7 +++++++ tests/test_ssm/test_ssm_boto3.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index 236c22005..29fd8820c 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -145,6 +145,13 @@ class SimpleSystemManagerResponse(BaseResponse): result = self.ssm_backend.get_parameter_history(name, with_decryption) + if result is None: + error = { + "__type": "ParameterNotFound", + "message": "Parameter {0} not found.".format(name), + } + return json.dumps(error), dict(status=400) + response = {"Parameters": []} for parameter_version in result: param_data = parameter_version.describe_response_object( diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 6b521132e..1b02536a1 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -879,6 +879,20 @@ def test_get_parameter_history_with_secure_string(): len(parameters_response).should.equal(3) +@mock_ssm +def test_get_parameter_history_missing_parameter(): + client = boto3.client("ssm", region_name="us-east-1") + + try: + client.get_parameter_history(Name="test_noexist") + raise RuntimeError("Should have failed") + except botocore.exceptions.ClientError as err: + err.operation_name.should.equal("GetParameterHistory") + err.response["Error"]["Message"].should.equal( + "Parameter test_noexist not found." + ) + + @mock_ssm def test_add_remove_list_tags_for_resource(): client = boto3.client("ssm", region_name="us-east-1")