Skip to content

Commit

Permalink
Merge pull request #62 from remind101/lockable_stacks
Browse files Browse the repository at this point in the history
Add ability to lock stacks
  • Loading branch information
phobologic committed Aug 12, 2015
2 parents 3d697e7 + c06375d commit 78f2d90
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 34 deletions.
3 changes: 3 additions & 0 deletions conf/empire/empire.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ stacks:
ImageName: ubuntu1404
- name: empireDB
class_path: stacker.blueprints.postgres.PostgresRDS
# this stack is locked, which means it will not update unless you pass the
# stack name "empireDB" on the command line with --force
locked: true
parameters:
<< : *vpc_parameters
InstanceType: ${empiredb_instance_type}
Expand Down
3 changes: 3 additions & 0 deletions conf/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ stacks:
ImageName: bastion
- name: myDB
class_path: stacker.blueprints.postgres.PostgresRDS
# this stack is locked, which means it will not update unless you pass the
# stack name "myDB" on the command line with --force
locked: true
parameters:
<< : *vpc_parameters
InstanceType: db.m3.medium
Expand Down
24 changes: 23 additions & 1 deletion stacker/actions/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@
logger = logging.getLogger(__name__)


def should_update(stack):
"""Tests whether a stack should be submitted for updates to CF.
Args:
stack (stacker.stack.Stack): The stack object to check.
Returns:
bool: If the stack should be updated, return True.
"""
if stack.locked:
if not stack.force:
logger.info("Stack %s locked and not in --force list. "
"Refusing to update.", stack.name)
return False
else:
logger.debug("Stack %s locked, but is in --force "
"list.", stack.name)
return True


class Action(BaseAction):
"""Responsible for building & coordinating CloudFormation stacks.
Expand Down Expand Up @@ -113,6 +133,8 @@ def _launch_stack(self, stack, **kwargs):
self.provider.create_stack(stack.fqn, template_url, parameters,
tags)
else:
if not should_update(stack):
return SKIPPED
self.provider.update_stack(stack.fqn, template_url, parameters,
tags)
except StackDidNotChange:
Expand Down Expand Up @@ -197,7 +219,7 @@ def run(self, outline=False, *args, **kwargs):
logger.info("Launching stacks: %s", ', '.join(plan.keys()))
plan.execute()
else:
plan.outline(execute_helper=True)
plan.outline()

def post_run(self, outline=False, *args, **kwargs):
"""Any steps that need to be taken after running the action."""
Expand Down
1 change: 1 addition & 0 deletions stacker/commands/stacker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ def configure(self, options, **kwargs):
environment=options.environment,
parameters=copy.deepcopy(options.parameters),
stack_names=options.stacks,
force_stacks=options.force,
)
options.context.load_config(options.config.read())
5 changes: 5 additions & 0 deletions stacker/commands/stacker/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def add_arguments(self, parser):
parser.add_argument("-o", "--outline", action="store_true",
help="Print an outline of what steps will be "
"taken to build the stacks")
parser.add_argument("--force", action="append", default=[],
metavar="STACKNAME", type=str,
help="If a stackname is provided to --force, it "
"will be updated, even if it is locked in "
"the config.")

def run(self, options, **kwargs):
super(Build, self).run(options, **kwargs)
Expand Down
18 changes: 12 additions & 6 deletions stacker/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ class Context(object):
"""

_optional_keys = ('environment', 'stack_names', 'parameters', 'mappings',
'config')

def __init__(self, namespace, **kwargs):
def __init__(self, namespace, environment=None, stack_names=None,
parameters=None, mappings=None, config=None,
force_stacks=None):
self.namespace = namespace
for key in self._optional_keys:
setattr(self, key, kwargs.get(key))
self.environment = environment or {}
self.stack_names = stack_names or []
self.parameters = parameters or {}
self.mappings = mappings or {}
self.config = config or {}
self.force_stacks = force_stacks or []

self._base_fqn = namespace.replace('.', '-').lower()

def load_config(self, conf_string):
Expand Down Expand Up @@ -50,6 +54,8 @@ def get_stacks(self):
context=self,
parameters=self.parameters,
mappings=self.mappings,
force=stack_def['name'] in self.force_stacks,
locked=stack_def.get('locked', False),
)
self._stacks.append(stack)
return self._stacks
Expand Down
10 changes: 1 addition & 9 deletions stacker/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def execute(self):

self._check_point()

def outline(self, level=logging.INFO, execute_helper=False):
def outline(self, level=logging.INFO):
"""Print an outline of the actions the plan is going to take.
The outline will represent the rough ordering of the steps that will be
Expand All @@ -185,10 +185,6 @@ def outline(self, level=logging.INFO, execute_helper=False):
Args:
level (Optional[int]): a valid log level that should be used to log
the outline
execute_helper (Optional[bool]): boolean for whether or not a line
instructing the user to pass the "force" flag should be
provided.
"""
steps = 1
logger.log(level, 'Plan "%s":', self.description)
Expand All @@ -206,10 +202,6 @@ def outline(self, level=logging.INFO, execute_helper=False):
step.status = COMPLETE
steps += 1

if execute_helper:
logger.log(level, 'To execute this plan, run with "-f, --force" '
'flag.')

def _check_point(self):
logger.info('Plan Status:')
for step_name, step in self.iteritems():
Expand Down
5 changes: 4 additions & 1 deletion stacker/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ def _gather_parameters(stack_def, builder_parameters):

class Stack(object):

def __init__(self, definition, context, parameters=None, mappings=None):
def __init__(self, definition, context, parameters=None, mappings=None,
locked=False, force=False):
self.name = definition['name']
self.fqn = context.get_fqn(self.name)
self.definition = definition
self.parameters = _gather_parameters(definition, parameters or {})
self.mappings = mappings
self.locked = locked
self.force = force
# XXX this is temporary until we remove passing context down to the
# blueprint
self.context = copy.deepcopy(context)
Expand Down
17 changes: 17 additions & 0 deletions stacker/tests/actions/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ def test_launch_stack_step_statuses(self):
plan = build_action._generate_plan()
_, step = plan.list_pending()[0]
step.stack = mock.MagicMock()
step.stack.locked = False

# mock provider shouldn't return a stack at first since it hasn't been
# launched
Expand Down Expand Up @@ -220,3 +221,19 @@ def test_launch_stack_step_statuses(self):
status = step.run()
self.assertEqual(status, SUBMITTED)
self.assertEqual(mock_provider.update_stack.call_count, 1)

def test_should_update(self):
test_scenario = namedtuple('test_scenario',
['locked', 'force', 'result'])
test_scenarios = (
test_scenario(locked=False, force=False, result=True),
test_scenario(locked=False, force=True, result=True),
test_scenario(locked=True, force=False, result=False),
test_scenario(locked=True, force=True, result=True)
)
mock_stack = mock.MagicMock(["locked", "force", "name"])
mock_stack.name = "test-stack"
for t in test_scenarios:
mock_stack.locked = t.locked
mock_stack.force = t.force
self.assertEqual(build.should_update(mock_stack), t.result)
17 changes: 0 additions & 17 deletions stacker/tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,6 @@ class TestContext(unittest.TestCase):
def setUp(self):
self.config = {'stacks': [{'name': 'stack1'}, {'name': 'stack2'}]}

def test_context_optional_keys_defaults(self):
context = Context('namespace')
for key in context._optional_keys:
self.assertTrue(hasattr(context, key))

def test_context_optional_keys_set(self):
context = Context(
'namespace',
environment={},
stack_names=['stack'],
mappings={},
config={},
)
for key in ['environment', 'mappings', 'config']:
self.assertEqual(getattr(context, key), {})
self.assertEqual(context.stack_names, ['stack'])

def test_context_get_stacks_specific_stacks(self):
context = Context('namespace', config=self.config,
stack_names=['stack2'])
Expand Down

0 comments on commit 78f2d90

Please sign in to comment.