Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Cancel and Wait commands #117

Merged
merged 1 commit into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 55 additions & 11 deletions formica/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argcomplete
import sys
import logging
import signal


from . import CHANGE_SET_FORMAT, __version__
Expand Down Expand Up @@ -64,7 +65,17 @@ def formica():
main(sys.argv[1:])


def signal_handler(sig, frame):
logger.info('Exiting because of ctrl-c')
sys.exit(0)


def add_signal_handler():
signal.signal(signal.SIGINT, signal_handler)


def main(cli_args):
add_signal_handler()
parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version', version='{}'.format(__version__))
subparsers = parser.add_subparsers(title='commands',
Expand Down Expand Up @@ -117,6 +128,20 @@ def main(cli_args):
add_config_file_argument(deploy_parser)
deploy_parser.set_defaults(func=deploy)

# Cancel Command Arguments
cancel_parser = subparsers.add_parser('cancel', description='Cancel a Stack Update')
add_aws_arguments(cancel_parser)
add_stack_argument(cancel_parser)
add_config_file_argument(cancel_parser)
cancel_parser.set_defaults(func=cancel)

# Wait Command Arguments
wait_parser = subparsers.add_parser('wait', description='Wait for a Stack to be deployed or removed')
add_aws_arguments(wait_parser)
add_stack_argument(wait_parser)
add_config_file_argument(wait_parser)
wait_parser.set_defaults(func=wait)

# Describe Command Arguments
describe_parser = subparsers.add_parser('describe', description='Describe the latest change-set of the stack')
add_aws_arguments(describe_parser)
Expand Down Expand Up @@ -458,27 +483,46 @@ def change(args):
change_set.describe()


def wait_for_stack(function):
def wait(args):
from .stack_waiter import StackWaiter
client = AWS.current_session().client('cloudformation')
stack_id = client.describe_stacks(StackName=args.stack)['Stacks'][0]['StackId']
last_event = client.describe_stack_events(StackName=args.stack)['StackEvents'][0]['EventId']
function(args, client)
StackWaiter(stack_id, client).wait(last_event)
return wait


@requires_stack
def deploy(args):
from .stack_waiter import StackWaiter
client = AWS.current_session().client('cloudformation')
last_event = client.describe_stack_events(StackName=args.stack)['StackEvents'][0]['EventId']
@wait_for_stack
def deploy(args, client):
logger.info('Deploying StackSet to {}'.format(args.stack))
client.execute_change_set(ChangeSetName=(CHANGE_SET_FORMAT.format(stack=args.stack)), StackName=args.stack)
StackWaiter(args.stack, client).wait(last_event)


@requires_stack
def remove(args):
from .stack_waiter import StackWaiter
client = AWS.current_session().client('cloudformation')
stack_id = client.describe_stacks(StackName=args.stack)['Stacks'][0]['StackId']
@wait_for_stack
def cancel(args, client):
logger.info('Canceling update for stack {}'.format(args.stack))
client.cancel_update_stack(StackName=args.stack)


@requires_stack
@wait_for_stack
def wait(args, client):
logger.info('Waiting for Stack {}'.format(args.stack))
pass


@requires_stack
@wait_for_stack
def remove(args, client):
logger.info('Removing Stack and waiting for it to be removed, ...')
last_event = client.describe_stack_events(StackName=args.stack)['StackEvents'][0]['EventId']
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)


@requires_stack
Expand Down
2 changes: 2 additions & 0 deletions formica/stack_waiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ def wait(self, last_event):
stack_status = self.client.describe_stacks(StackName=self.stack)['Stacks'][0]['StackStatus']
if stack_status in SUCCESSFUL_STATES:
finished = True
logger.info("Stack Change Successful: {}".format(stack_status))
elif stack_status in FAILED_STATES:
logger.info("Stack Change Failed: {}".format(stack_status))
sys.exit(1)

def __create_table(self):
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/test_cancel_wait.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from mock import Mock

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


@pytest.fixture
def logger(mocker):
return mocker.patch('formica.cli.logger')


@pytest.fixture
def stack_waiter(mocker):
return mocker.patch('formica.stack_waiter.StackWaiter')


@pytest.fixture
def session(mocker):
return mocker.patch('boto3.session.Session')


@pytest.fixture
def client(mocker, session):
client_mock = Mock()
session.return_value.client.return_value = client_mock
return client_mock


def test_cancel_stack_update(client, stack_waiter):
client.describe_stacks.return_value = {'Stacks': [{'StackId': STACK_ID}]}
client.describe_stack_events.return_value = {'StackEvents': [{'EventId': EVENT_ID}]}
cli.main(['cancel', '--stack', STACK])
client.cancel_update_stack.assert_called_with(StackName=STACK)


def test_wait(client, stack_waiter):
client.describe_stacks.return_value = {'Stacks': [{'StackId': STACK_ID}]}
client.describe_stack_events.return_value = {'StackEvents': [{'EventId': EVENT_ID}]}
cli.main(['wait', '--stack', STACK])
5 changes: 3 additions & 2 deletions tests/unit/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from botocore.exceptions import NoCredentialsError

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


@pytest.fixture
Expand Down Expand Up @@ -49,9 +49,10 @@ def test_executes_change_set_and_waits(session, stack_waiter):
cf_client_mock = Mock()
session.return_value.client.return_value = cf_client_mock
cf_client_mock.describe_stack_events.return_value = {'StackEvents': [{'EventId': EVENT_ID}]}
cf_client_mock.describe_stacks.return_value = {'Stacks': [{'StackId': STACK_ID}]}

cli.main(['deploy', '--stack', STACK, '--profile', PROFILE, '--region', REGION])
cf_client_mock.describe_stack_events.assert_called_with(StackName=STACK)
cf_client_mock.execute_change_set.assert_called_with(ChangeSetName=CHANGESETNAME, StackName=STACK)
stack_waiter.assert_called_with(STACK, cf_client_mock)
stack_waiter.assert_called_with(STACK_ID, cf_client_mock)
stack_waiter.return_value.wait.assert_called_with(EVENT_ID)