Skip to content

Commit

Permalink
Merge 72232eb into 54677f8
Browse files Browse the repository at this point in the history
  • Loading branch information
flomotlik committed Dec 16, 2017
2 parents 54677f8 + 72232eb commit 26af43e
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 20 deletions.
2 changes: 1 addition & 1 deletion formica/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import sys

__version__ = '0.6.0'
__version__ = '0.6.1'

CHANGE_SET_FORMAT = "{stack}-change-set"

Expand Down
8 changes: 6 additions & 2 deletions formica/change_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, stack, client):
self.stack = stack
self.client = client

def create(self, template, change_set_type, parameters=[], tags=[], capabilities=[]):
def create(self, template, change_set_type, parameters=[], tags=[], capabilities=[], role_arn=None):
optional_arguments = {}
if parameters:
optional_arguments['Parameters'] = [
Expand All @@ -26,12 +26,16 @@ def create(self, template, change_set_type, parameters=[], tags=[], capabilities
if tags:
optional_arguments['Tags'] = [{'Key': key, 'Value': value, } for (key, value) in
tags.items()]
if role_arn:
optional_arguments['RoleARN'] = role_arn
if capabilities:
optional_arguments['Capabilities'] = capabilities
if change_set_type == 'UPDATE':
self.remove_existing_changeset()

self.client.create_change_set(StackName=self.stack, TemplateBody=template,
ChangeSetName=self.name, ChangeSetType=change_set_type, **optional_arguments)
ChangeSetName=self.name, ChangeSetType=change_set_type,
**optional_arguments)
logger.info('Change set submitted, waiting for CloudFormation to calculate changes ...')
waiter = self.client.get_waiter('change_set_create_complete')
try:
Expand Down
18 changes: 15 additions & 3 deletions formica/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'stack': str,
'tags': dict,
'parameters': dict,
'role_arn': str,
'region': str,
'profile': str,
'capabilities': list,
Expand Down Expand Up @@ -76,6 +77,7 @@ def main(cli_args):
add_stack_parameters_argument(new_parser)
add_stack_tags_argument(new_parser)
add_capabilities_argument(new_parser)
add_role_arn_argument(new_parser)
add_config_file_argument(new_parser)
add_stack_variables_argument(new_parser)
new_parser.set_defaults(func=new)
Expand All @@ -87,6 +89,7 @@ def main(cli_args):
add_stack_parameters_argument(change_parser)
add_stack_tags_argument(change_parser)
add_capabilities_argument(change_parser)
add_role_arn_argument(change_parser)
add_config_file_argument(change_parser)
add_stack_variables_argument(change_parser)
change_parser.set_defaults(func=change)
Expand Down Expand Up @@ -124,6 +127,7 @@ def main(cli_args):
remove_parser = subparsers.add_parser('remove', description='Remove the configured stack')
add_aws_arguments(remove_parser)
add_stack_argument(remove_parser)
add_role_arn_argument(remove_parser)
add_config_file_argument(remove_parser)
remove_parser.set_defaults(func=remove)

Expand Down Expand Up @@ -196,6 +200,10 @@ def add_capabilities_argument(parser):
metavar='Cap1 Cap2', nargs='*')


def add_role_arn_argument(parser):
parser.add_argument('--role-arn', help='Set a separate role ARN to pass to the stack')


def add_config_file_argument(parser):
parser.add_argument('--config-file', '-c', type=argparse.FileType('r'), help='Set the config file to use')

Expand Down Expand Up @@ -270,7 +278,7 @@ def change(args):

change_set = ChangeSet(stack=args.stack, client=client)
change_set.create(template=loader.template(indent=None), change_set_type='UPDATE', parameters=args.parameters,
tags=args.tags, capabilities=args.capabilities)
tags=args.tags, capabilities=args.capabilities, role_arn=args.role_arn)
change_set.describe()


Expand All @@ -288,7 +296,10 @@ def remove(args):
stack_id = client.describe_stacks(StackName=args.stack)['Stacks'][0]['StackId']
logger.info('Removing Stack and waiting for it to be removed, ...')
last_event = client.describe_stack_events(StackName=args.stack)['StackEvents'][0]['EventId']
client.delete_stack(StackName=args.stack)
if args.role_arn:
client.delete_stack(StackName=args.stack, RoleARN=args.role_arn)
else:
client.delete_stack(StackName=args.stack)
StackWaiter(stack_id, client).wait(last_event)


Expand All @@ -300,7 +311,7 @@ def new(args):
logger.info('Creating change set for new stack, ...')
change_set = ChangeSet(stack=args.stack, client=client)
change_set.create(template=loader.template(indent=None), change_set_type='CREATE', parameters=args.parameters,
tags=args.tags, capabilities=args.capabilities)
tags=args.tags, capabilities=args.capabilities, role_arn=args.role_arn)
change_set.describe()
logger.info('Change set created, please deploy')

Expand All @@ -313,6 +324,7 @@ def load_config_file(args, config_file):
logger.error(e.__str__())
sys.exit(1)
for key, value in config_file_args.items():
key = key.replace('-', '_')
if key in CONFIG_FILE_ARGUMENTS.keys():
config_type = CONFIG_FILE_ARGUMENTS[key]
if not args_dict.get(key) and value:
Expand Down
4 changes: 4 additions & 0 deletions stacks/test.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
stack: formica-test-stack
role-arn: arn:aws:iam::080551076419:role/test/1/formica-test-stack-TestRole1-NPMDYV78VC8J
capabilities:
- CAPABILITY_IAM
30 changes: 30 additions & 0 deletions stacks/test.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Resources:
S3TestBucket:
Type: AWS::S3::Bucket

S3TestBucket2:
Type: AWS::S3::Bucket

TestRole1:
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service:
- "cloudformation.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: /test/1/
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action: "*"
Resource: "*"
PolicyName: TestRole1
Type: AWS::IAM::Role
3 changes: 2 additions & 1 deletion tests/unit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
STACK = 'my-stack'
STACK_ID = 'my-stack-id'
TEMPLATE = 'MYTEMPLATE'
ROLE_ARN = 'arn:aws:iam::1234567890:role/some-stack-role'
CHANGESETNAME = '{}-change-set'.format(STACK)
CHANGE_SET_TYPE = 'WHATEVER'
CHANGE_SET_PARAMETERS = {'A': 'B', 'B': 'C'}
Expand All @@ -20,4 +21,4 @@
EVENT_ID = 'SomeEventID'
STACK_EVENTS = {'StackEvents': [{'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': '7e7e22e0-ec85-11e6-a72f-50a68a770ce6', 'StackName': 'teststack', 'LogicalResourceId': 'teststack', 'PhysicalResourceId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'ResourceType': 'AWS::CloudFormation::Stack', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 17, 611000, tzinfo=tzlocal()), 'ResourceStatus': 'UPDATE_COMPLETE', "ResourceStatusReason": "Resource creation Initiated"}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket14-bc35d70a-3df5-45d4-afaa-c06b536a50cd', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket14', 'PhysicalResourceId': 'teststack-deploymentbucket14-1r1yxsi27kclv', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 976000), 'ResourceStatus': 'DELETE_COMPLETE'}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket18-f9a5ef79-307a-4919-a293-b74be40e19b2', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket18', 'PhysicalResourceId': 'teststack-deploymentbucket18-iy7lt61peqvp', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 868000), 'ResourceStatus': 'DELETE_COMPLETE'}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket3-7c92066b-c2e7-427a-ab29-53b928925473', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket3', 'PhysicalResourceId': 'teststack-deploymentbucket3-51e2v1veq7go', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 852000), 'ResourceStatus': 'DELETE_COMPLETE'}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket15-4ce4845e-f072-4d69-9aae-d2e8105dc0a8', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket15', 'PhysicalResourceId': 'teststack-deploymentbucket15-2tdubysims21', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 712000), 'ResourceStatus': 'DELETE_COMPLETE'}]} # noqa
LIST_STACK_RESOURCES = {'ResponseMetadata': {'RetryAttempts': 0, 'HTTPStatusCode': 200, 'RequestId': '516e9109-f819-11e6-816b-69534f0a2126', 'HTTPHeaders': {'x-amzn-requestid': '516e9109-f819-11e6-816b-69534f0a2126', 'vary': 'Accept-Encoding', 'content-length': '2121', 'content-type': 'text/xml', 'date': 'Tue, 21 Feb 2017 09:37:10 GMT'}}, u'StackResourceSummaries': [{u'ResourceType': 'AWS::Route53::HostedZone', u'PhysicalResourceId': 'ZAYGDOKFPYFK6', u'LastUpdatedTimestamp': datetime.datetime(2017, 1, 13, 16, 13, 25, 978000, tzinfo=tzlocal()), u'ResourceStatus': 'CREATE_COMPLETE', u'LogicalResourceId': 'FlomotlikMe'}]} # noqa
FULL_CONFIG_FILE = {'capabilities': CHANGE_SET_CAPABILITIES, 'profile': PROFILE, 'region': REGION, 'stack': STACK, 'parameters': CHANGE_SET_PARAMETERS, 'tags': CHANGE_SET_STACK_TAGS} # noqa
FULL_CONFIG_FILE = {'capabilities': CHANGE_SET_CAPABILITIES, 'profile': PROFILE, 'region': REGION, 'stack': STACK, 'parameters': CHANGE_SET_PARAMETERS, 'tags': CHANGE_SET_STACK_TAGS, 'role-arn': ROLE_ARN} # noqa
8 changes: 4 additions & 4 deletions tests/unit/test_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_change_creates_update_change_set(change_set, session, loader):
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='UPDATE',
parameters=None,
tags=None, capabilities=None)
tags=None, capabilities=None, role_arn=None)
change_set.return_value.describe.assert_called_once()


Expand All @@ -39,7 +39,7 @@ def test_change_uses_parameters_for_update(change_set, session, loader):
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='UPDATE',
parameters={'A': 'B', 'C': 'D'}, tags=None,
capabilities=None)
capabilities=None, role_arn=None)
change_set.return_value.describe.assert_called_once()


Expand All @@ -59,7 +59,7 @@ def test_change_uses_tags_for_creation(change_set, session, loader):
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='UPDATE',
parameters=None,
tags={'A': 'B', 'C': 'D'}, capabilities=None)
tags={'A': 'B', 'C': 'D'}, capabilities=None, role_arn=None)


def test_change_tests_tag_format(capsys):
Expand All @@ -79,4 +79,4 @@ def test_change_uses_capabilities_for_creation(change_set, session, loader):
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='UPDATE',
parameters=None,
tags=None, capabilities=['A', 'B'])
tags=None, capabilities=['A', 'B'], role_arn=None)
23 changes: 21 additions & 2 deletions tests/unit/test_change_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from botocore.exceptions import WaiterError, ClientError

from formica.change_set import ChangeSet, CHANGE_SET_HEADER
from tests.unit.constants import STACK, TEMPLATE, CHANGE_SET_TYPE, CHANGESETNAME, CHANGESETCHANGES, \
CHANGE_SET_PARAMETERS, CHANGESETCHANGES_WITH_DUPLICATE_CHANGED_PARAMETER, CHANGE_SET_STACK_TAGS
from tests.unit.constants import (
STACK, TEMPLATE, CHANGE_SET_TYPE, CHANGESETNAME, CHANGESETCHANGES,
CHANGE_SET_PARAMETERS, ROLE_ARN, CHANGE_SET_STACK_TAGS,
CHANGESETCHANGES_WITH_DUPLICATE_CHANGED_PARAMETER,
)


@pytest.fixture
Expand Down Expand Up @@ -69,6 +72,22 @@ def test_submits_changeset_with_stack_tags():
StackName=STACK, ChangeSetName=CHANGESETNAME)


def test_submits_changeset_with_role_arn():
cf_client_mock = Mock()
change_set = ChangeSet(STACK, cf_client_mock)

change_set.create(template=TEMPLATE, change_set_type=CHANGE_SET_TYPE, role_arn=ROLE_ARN)

cf_client_mock.create_change_set.assert_called_with(
StackName=STACK, TemplateBody=TEMPLATE,
ChangeSetName=CHANGESETNAME, ChangeSetType=CHANGE_SET_TYPE, RoleARN=ROLE_ARN)

cf_client_mock.get_waiter.assert_called_with(
'change_set_create_complete')
cf_client_mock.get_waiter.return_value.wait.assert_called_with(
StackName=STACK, ChangeSetName=CHANGESETNAME)


def test_submits_changeset_with_capabilities():
cf_client_mock = Mock()
change_set = ChangeSet(STACK, cf_client_mock)
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/test_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from formica import cli
from tests.unit.constants import (REGION, PROFILE, STACK,
CHANGE_SET_PARAMETERS, CHANGE_SET_STACK_TAGS,
FULL_CONFIG_FILE, CHANGE_SET_CAPABILITIES)
FULL_CONFIG_FILE, CHANGE_SET_CAPABILITIES,
ROLE_ARN)


@pytest.fixture
Expand All @@ -31,6 +32,7 @@ def test_loads_config_file(mocker, tmpdir, session):
assert call_args.parameters == CHANGE_SET_PARAMETERS
assert call_args.tags == CHANGE_SET_STACK_TAGS
assert call_args.capabilities == CHANGE_SET_CAPABILITIES
assert call_args.role_arn == ROLE_ARN


def test_exception_with_wrong_config_type(mocker, tmpdir, session, logger):
Expand Down
24 changes: 19 additions & 5 deletions tests/unit/test_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def test_create_changeset_for_new_stack(change_set, session, loader):
cli.main(['new', '--stack', STACK, '--profile', PROFILE, '--region', REGION])
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='CREATE',
parameters=None, tags=None, capabilities=None)
parameters=None, tags=None, capabilities=None,
role_arn=None)
change_set.return_value.describe.assert_called_once()


Expand All @@ -39,7 +40,7 @@ def test_new_uses_parameters_for_creation(change_set, session, loader):
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='CREATE',
parameters={'A': 'B', 'C': 'D'}, tags=None,
capabilities=None)
capabilities=None, role_arn=None)


def test_new_uses_tags_for_creation(change_set, session, loader):
Expand All @@ -50,7 +51,20 @@ def test_new_uses_tags_for_creation(change_set, session, loader):
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='CREATE',
parameters=None,
tags={'A': 'B', 'C': 'D'}, capabilities=None)
tags={'A': 'B', 'C': 'D'}, capabilities=None,
role_arn=None)


def test_new_role_arn_for_creation(change_set, session, loader):
client_mock = Mock()
session.return_value.client.return_value = client_mock
loader.return_value.template.return_value = TEMPLATE
cli.main(['new', '--stack', STACK, '--profile', PROFILE, '--region', REGION, '--role-arn', 'arn:aws:foobarbaz'])
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='CREATE',
parameters=None,
tags=None, capabilities=None,
role_arn='arn:aws:foobarbaz')


def test_new_tests_parameter_format(capsys):
Expand All @@ -69,5 +83,5 @@ def test_new_uses_capabilities_for_creation(change_set, session, loader):
cli.main(['new', '--stack', STACK, '--capabilities', 'A', 'B'])
change_set.assert_called_with(stack=STACK, client=client_mock)
change_set.return_value.create.assert_called_once_with(template=TEMPLATE, change_set_type='CREATE',
parameters=None,
tags=None, capabilities=['A', 'B'])
parameters=None, tags=None,
capabilities=['A', 'B'], role_arn=None)
19 changes: 18 additions & 1 deletion tests/unit/test_remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from mock import patch, Mock

from formica import cli
from tests.unit.constants import REGION, PROFILE, STACK, EVENT_ID, STACK_ID
from tests.unit.constants import REGION, PROFILE, STACK, EVENT_ID, STACK_ID, ROLE_ARN


class TestRemove(unittest.TestCase):
Expand All @@ -22,3 +22,20 @@ def test_removes_stack(self, change_set, session, loader, stack_waiter):
client_mock.delete_stack.assert_called_with(StackName=STACK)
stack_waiter.assert_called_with(STACK_ID, client_mock)
stack_waiter.return_value.wait.assert_called_with(EVENT_ID)

@patch('formica.cli.StackWaiter')
@patch('formica.cli.Loader')
@patch('formica.aws.Session')
@patch('formica.cli.ChangeSet')
def test_removes_stack_with_role(self, change_set, session, loader, stack_waiter):
client_mock = Mock()
client_mock.describe_stacks.return_value = {'Stacks': [{'StackId': STACK_ID}]}
session.return_value.client.return_value = client_mock
client_mock.describe_stack_events.return_value = {'StackEvents': [{'EventId': EVENT_ID}]}

cli.main(['remove', '--stack', STACK, '--profile', PROFILE, '--region', REGION, '--role-arn', ROLE_ARN])
client_mock.describe_stacks.assert_called_with(StackName=STACK)
client_mock.describe_stack_events.assert_called_with(StackName=STACK)
client_mock.delete_stack.assert_called_with(StackName=STACK, RoleARN=ROLE_ARN)
stack_waiter.assert_called_with(STACK_ID, client_mock)
stack_waiter.return_value.wait.assert_called_with(EVENT_ID)

0 comments on commit 26af43e

Please sign in to comment.