generated from Sceptre/sceptre-resolver-template
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Resolves #620] move sceptre-ssm-resolver (#2)
* Move the sceptre-ssm-resolver from https://github.com/zaro0508/sceptre-ssm-resolver to the official Sceptre github Org. * Slightly refactored to include unit tests. * add boto3 to requirements * replaced sceptre-core with sceptre in requirements * work around for ImportError: CircleCi buld reports ImportError. Use work around suggested at: https://stackoverflow.com/questions/10253826/path-issue-with-pytest-importerror-no-module-named-yadayadayada
- Loading branch information
Showing
12 changed files
with
282 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -137,3 +137,4 @@ Session.vim | |
tags | ||
# Persistent undo | ||
[._]*.un~ | ||
temp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,39 @@ | ||
# README | ||
--- | ||
layout: docs | ||
title: Resolvers | ||
--- | ||
|
||
Add your resolver readme here. Remember to include the following: | ||
# Overview | ||
|
||
- Tell people how to install it (e.g. pip install ...). | ||
- Be clear about the purpose of the resolver, its capabilities and limitations. | ||
- Tell people how to use it. | ||
- Give examples of the resolver in use. | ||
The purpose of this resolver is to retrieve values from the AWS SSM. | ||
|
||
Read our wiki to learn how to use this repo: | ||
https://github.com/Sceptre/project/wiki/Sceptre-Resolver-Template | ||
## Available Resolvers | ||
|
||
If you have any questions or encounter an issue | ||
[please open an issue](https://github.com/Sceptre/project/issues/new) | ||
### ssm | ||
|
||
Fetches the value stored in AWS SSM Parameter Store. | ||
|
||
Syntax: | ||
|
||
```yaml | ||
parameter|sceptre_user_data: | ||
<name>: !ssm /prefix/param | ||
``` | ||
|
||
#### Example: | ||
|
||
Add a secure string to the SSM parameter store | ||
``` | ||
aws ssm put-parameter --name /dev/DbPassword --value "mysecret" \ | ||
--key-id alias/dev/kmskey --type "SecureString" | ||
``` | ||
|
||
Setup sceptre template to retrieve and decrypt from parameter store | ||
``` | ||
parameters: | ||
database_password: !ssm /dev/DbPassword | ||
``` | ||
|
||
Run sceptre with a user or role that has access to the secret. | ||
Sceptre will retrieve "mysecret" from the parameter store and passes | ||
it to the cloudformation _database_password_ paramter. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
|
||
class ParameterNotFoundError(Exception): | ||
""" | ||
Error raised when the SSM parameter does not exist | ||
""" | ||
pass |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import abc | ||
import six | ||
import logging | ||
|
||
from botocore.exceptions import ClientError | ||
|
||
from sceptre.resolvers import Resolver | ||
from resolver.exceptions import ParameterNotFoundError | ||
|
||
TEMPLATE_EXTENSION = ".yaml" | ||
|
||
|
||
@six.add_metaclass(abc.ABCMeta) | ||
class SsmBase(Resolver): | ||
""" | ||
A abstract base class which provides methods for getting SSM parameters. | ||
""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
self.logger = logging.getLogger(__name__) | ||
super(SsmBase, self).__init__(*args, **kwargs) | ||
|
||
def _get_parameter_value(self, param, profile=None, region=None): | ||
""" | ||
Attempts to get the SSM parameter named by ``param`` | ||
:param param: The name of the SSM parameter in which to return. | ||
:type param: str | ||
:returns: SSM parameter value. | ||
:rtype: str | ||
:raises: KeyError | ||
""" | ||
response = self._request_parameter(param, profile, region) | ||
|
||
try: | ||
return response['Parameter']['Value'] | ||
except KeyError: | ||
self.logger.error("%s - Invalid response looking for: %s", | ||
self.stack.name, param) | ||
raise | ||
|
||
def _request_parameter(self, param, profile=None, region=None): | ||
""" | ||
Communicates with AWS CloudFormation to fetch SSM parameters. | ||
:returns: The decoded value of the parameter | ||
:rtype: dict | ||
:raises: resolver.exceptions.ParameterNotFoundError | ||
""" | ||
connection_manager = self.stack.connection_manager | ||
|
||
try: | ||
response = connection_manager.call( | ||
service="ssm", | ||
command="get_parameter", | ||
kwargs={"Name": param, | ||
"WithDecryption": True}, | ||
profile=profile, | ||
region=region, | ||
) | ||
except ClientError as e: | ||
if "ParameterNotFound" in e.response["Error"]["Code"]: | ||
self.logger.error("%s - ParameterNotFound: %s", | ||
self.stack.name, param) | ||
raise ParameterNotFoundError(e.response["Error"]["Message"]) | ||
else: | ||
raise e | ||
else: | ||
return response | ||
|
||
|
||
class SSM(SsmBase): | ||
""" | ||
Resolver for retrieving the value of an SSM parameter. | ||
:param argument: The parameter name to get. | ||
:type argument: str | ||
""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(SSM, self).__init__(*args, **kwargs) | ||
|
||
def resolve(self): | ||
""" | ||
Retrieves the value of SSM parameter | ||
:returns: The decoded value of the SSM parameter | ||
:rtype: str | ||
""" | ||
self.logger.debug( | ||
"Resolving SSM parameter: {0}".format(self.argument) | ||
) | ||
|
||
value = None | ||
profile = self.stack.profile | ||
region = self.stack.region | ||
if self.argument: | ||
param = self.argument | ||
value = self._get_parameter_value(param, profile, region) | ||
|
||
return value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import pytest | ||
from mock import MagicMock, patch, sentinel | ||
|
||
from botocore.exceptions import ClientError | ||
|
||
from sceptre.connection_manager import ConnectionManager | ||
from sceptre.stack import Stack | ||
|
||
from resolver.ssm import SSM, SsmBase | ||
from resolver.exceptions import ParameterNotFoundError | ||
|
||
|
||
class TestSsmResolver(object): | ||
|
||
@patch( | ||
"resolver.ssm.SSM._get_parameter_value" | ||
) | ||
def test_resolve(self, mock_get_parameter_value): | ||
stack = MagicMock(spec=Stack) | ||
stack.profile = "test_profile" | ||
stack.region = "test_region" | ||
stack.dependencies = [] | ||
stack._connection_manager = MagicMock(spec=ConnectionManager) | ||
stack_ssm_resolver = SSM( | ||
"/dev/DbPassword", stack | ||
) | ||
mock_get_parameter_value.return_value = "parameter_value" | ||
stack_ssm_resolver.resolve() | ||
mock_get_parameter_value.assert_called_once_with( | ||
"/dev/DbPassword", "test_profile", "test_region" | ||
) | ||
assert stack.dependencies == [] | ||
|
||
|
||
class MockSsmBase(SsmBase): | ||
""" | ||
MockBaseResolver inherits from the abstract base class | ||
SsmBase, and implements the abstract methods. It is used | ||
to allow testing on SsmBase, which is not otherwise | ||
instantiable. | ||
""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(MockSsmBase, self).__init__(*args, **kwargs) | ||
|
||
def resolve(self): | ||
pass | ||
|
||
|
||
class TestSsmBase(object): | ||
|
||
def setup_method(self, test_method): | ||
self.stack = MagicMock(spec=Stack) | ||
self.stack.name = "test_name" | ||
self.stack._connection_manager = MagicMock( | ||
spec=ConnectionManager | ||
) | ||
self.base_ssm = MockSsmBase( | ||
None, self.stack | ||
) | ||
|
||
@patch( | ||
"resolver.ssm.SsmBase._request_parameter" | ||
) | ||
def test_get_parameter_value_with_valid_key(self, mock_request_parameter): | ||
mock_request_parameter.return_value = { | ||
"Parameter": { | ||
"Name": "/dev/DbPassword", | ||
"Type": "SecureString", | ||
"Value": "Secret", | ||
"Version": 1, | ||
"LastModifiedDate": 1531863312.945, | ||
"ARN": "arn:aws:ssm:us-east-1:111111111111:parameter/dev/DbPassword" | ||
} | ||
} | ||
response = self.base_ssm._get_parameter_value("/dev/DbPassword") | ||
assert response == "Secret" | ||
|
||
@patch( | ||
"resolver.ssm.SsmBase._request_parameter" | ||
) | ||
def test_get_parameter_value_with_invalid_response(self, mock_request_parameter): | ||
mock_request_parameter.return_value = { | ||
"Parameter": { | ||
"Name": "/dev/DbPassword" | ||
} | ||
} | ||
|
||
with pytest.raises(KeyError): | ||
self.base_ssm._get_parameter_value(None) | ||
|
||
def test_request_parameter_with_unkown_boto_error(self): | ||
self.stack.connection_manager.call.side_effect = ClientError( | ||
{ | ||
"Error": { | ||
"Code": "500", | ||
"Message": "Boom!" | ||
} | ||
}, | ||
sentinel.operation | ||
) | ||
|
||
with pytest.raises(ClientError): | ||
self.base_ssm._request_parameter(None) | ||
|
||
def test_request_parameter_with_parameter_not_found(self): | ||
self.stack.connection_manager.call.side_effect = ClientError( | ||
{ | ||
"Error": { | ||
"Code": "ParameterNotFound", | ||
"Message": "Boom!" | ||
} | ||
}, | ||
sentinel.operation | ||
) | ||
|
||
with pytest.raises(ParameterNotFoundError): | ||
self.base_ssm._request_parameter(None) |