Skip to content

Commit

Permalink
Merge pull request #32 from remind101/gh-22
Browse files Browse the repository at this point in the history
Stack specific parameters on CLI
  • Loading branch information
phobologic committed May 19, 2015
2 parents 44825e5 + 1889bb7 commit 1671a90
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 24 deletions.
2 changes: 2 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
machine:
timezone: America/Los_Angeles
114 changes: 90 additions & 24 deletions stacker/builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import time
import copy

logger = logging.getLogger(__name__)

Expand All @@ -14,11 +15,10 @@


class MissingParameterException(Exception):
def __init__(self, blueprint, parameters):
self.blueprint = blueprint
self.parameter = parameters
self.message = "Blueprint %s missing required parameters: %s" % (
blueprint, ', '.join(parameters))
def __init__(self, parameters):
self.parameters = parameters
self.message = ("Missing required parameters: %s" %
', '.join(parameters))

def __str__(self):
return self.message
Expand All @@ -37,6 +37,73 @@ def stack_template_url(bucket_name, blueprint):
return "https://s3.amazonaws.com/%s/%s" % (bucket_name, key_name)


def gather_parameters(stack_def, builder_parameters):
""" Merges builder provided & stack defined parameters.
Ensures that more specificly defined parameters (ie: parameters defined
specifically for the given stack: stack_name::parameter) override less
specific parameters provided by the builder.
Order of precedence:
- builder defined stack specific (stack_name::parameter)
- builder defined non-specific (parameter)
- stack_def defined
"""
parameters = copy.deepcopy(stack_def.get('parameters', {}))
stack_specific_params = {}
for key, value in builder_parameters.iteritems():
stack = None
if "::" in key:
stack, key = key.split("::", 1)
if not stack:
# Non-stack specific, go ahead and add it
parameters[key] = value
continue
# Gather stack specific params for later
if stack == stack_def['name']:
stack_specific_params[key] = value
# Now update stack parameters with the stack specific parameters
# ensuring they override generic parameters
parameters.update(stack_specific_params)
return parameters


def handle_missing_parameters(params, required_params, existing_stack=None):
""" Handles any missing parameters.
If an existing_stack is provided, look up missing parameters there.
Args:
params (dict): key/value dictionary of stack definition parameters
required_params (list): A list of required parameter names.
existing_stack (Stack): A boto.cloudformation.stack.Stack object.
If provided, will be searched for any
missing parameters.
Returns:
list of tuples: The final list of key/value pairs returned as a
list of tuples.
Raises:
MissingParameterException: Raised if a required parameter is
still missing.
"""
missing_params = list(set(required_params) - set(params.keys()))
if existing_stack:
stack_params = {p.key: p.value for p in existing_stack.parameters}
for p in missing_params:
if p in stack_params:
value = stack_params[p]
logger.debug("Using parameter %s from existing stack: %s",
p, value)
params[p] = value
final_missing = list(set(required_params) - set(params.keys()))
if final_missing:
raise MissingParameterException(final_missing)

return params.items()


class Builder(object):
""" Responsible for building & coordinating CloudFormation stacks.
Expand Down Expand Up @@ -164,13 +231,23 @@ def build_blueprint(self, stack_context):
self.plan[stack_name].blueprint = blueprint
return blueprint

def resolve_parameters(self, parameters, blueprint, existing_stack=None):
def resolve_parameters(self, parameters, blueprint):
""" Resolves parameters for a given blueprint.
Given a list of parameters, first discard any parameters that the
blueprint does not use. Then, if a remaining parameter is in the format
<stack_name>::<output_name>, pull that output from the foreign
stack.
Args:
parameters (dict): A dictionary of parameters provided by the
stack definition
blueprint (Blueprint): A stacker.blueprint.base.Blueprint object
that is having the parameters applied to
it.
Returns:
dict: The resolved parameters.
"""
params = {}
blueprint_params = blueprint.parameters
Expand All @@ -186,22 +263,7 @@ def resolve_parameters(self, parameters, blueprint, existing_stack=None):
self.get_outputs(stack_name)
value = self.outputs[stack_name][output]
params[k] = value
# Deal w/ missing parameters
required_params = [k for k, v in blueprint.required_parameters]
missing_params = list(set(required_params) - set(params.keys()))
if existing_stack:
stack_params = {p.key: p.value for p in existing_stack.parameters}
for p in missing_params:
if p in stack_params:
value = stack_params[p]
logger.debug("Using parameter %s from existing stack: %s",
p, value)
params[p] = value
final_missing = list(set(required_params) - set(params.keys()))
if final_missing:
raise MissingParameterException(blueprint.name, final_missing)

return params.items()
return params

def get_stack(self, stack_full_name):
""" Give a stacks full name, query for the boto Stack object.
Expand Down Expand Up @@ -270,7 +332,10 @@ def launch_stack(self, stack_name, blueprint):
template_url = self.s3_stack_push(blueprint)
tags = self.build_stack_tags(stack_context, template_url)
parameters = self.resolve_parameters(stack_context.parameters,
blueprint, stack)
blueprint)
required_params = [k for k, v in blueprint.required_parameters]
parameters = handle_missing_parameters(parameters, required_params,
stack)
submitted = False
if not stack:
submitted = self.create_stack(full_name, template_url, parameters,
Expand Down Expand Up @@ -333,8 +398,9 @@ def build_plan(self, stack_definitions):
plan = Plan()
for stack_def in stack_definitions:
# Combine the Builder parameters with the stack parameters
stack_def['parameters'].update(self.parameters)
stack_def['namespace'] = self.namespace
stack_def['parameters'] = gather_parameters(stack_def,
self.parameters)
plan.add(stack_def)
return plan

Expand Down
86 changes: 86 additions & 0 deletions stacker/tests/test_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import unittest
from collections import namedtuple

from stacker.builder import (
gather_parameters, handle_missing_parameters, MissingParameterException
)


class TestGatherParameters(unittest.TestCase):
def setUp(self):
self.sd = {"name": "test"}

def test_empty_parameters(self):
builder_parameters = {}
self.assertEqual({}, gather_parameters(self.sd, builder_parameters))

def test_generic_builder_override(self):
sdef = self.sd
sdef["parameters"] = {"Address": "10.0.0.1", "Foo": "BAR"}
builder_parameters = {"Address": "192.168.1.1"}
result = gather_parameters(sdef, builder_parameters)
self.assertEqual(result["Address"], "192.168.1.1")
self.assertEqual(result["Foo"], "BAR")

def test_stack_specific_override(self):
sdef = self.sd
sdef["parameters"] = {"Address": "10.0.0.1", "Foo": "BAR"}
builder_parameters = {"test::Address": "192.168.1.1"}
result = gather_parameters(sdef, builder_parameters)
self.assertEqual(result["Address"], "192.168.1.1")
self.assertEqual(result["Foo"], "BAR")

def test_invalid_stack_specific_override(self):
sdef = self.sd
sdef["parameters"] = {"Address": "10.0.0.1", "Foo": "BAR"}
builder_parameters = {"FAKE::Address": "192.168.1.1"}
result = gather_parameters(sdef, builder_parameters)
self.assertEqual(result["Address"], "10.0.0.1")
self.assertEqual(result["Foo"], "BAR")

def test_specific_vs_generic_builder_override(self):
sdef = self.sd
sdef["parameters"] = {"Address": "10.0.0.1", "Foo": "BAR"}
builder_parameters = {
"test::Address": "192.168.1.1",
"Address": "10.0.0.1"}
result = gather_parameters(sdef, builder_parameters)
self.assertEqual(result["Address"], "192.168.1.1")
self.assertEqual(result["Foo"], "BAR")


Parameter = namedtuple('Parameter', ['key', 'value'])


class MockStack(object):
def __init__(self, parameters):
self.parameters = []
for k, v in parameters.items():
self.parameters.append(Parameter(key=k, value=v))


class TestHandleMissingParameters(unittest.TestCase):
def test_gather_missing_from_stack(self):
stack_params = {'Address': '10.0.0.1'}
stack = MockStack(stack_params)
def_params = {}
required = ['Address']
self.assertEqual(
handle_missing_parameters(def_params, required, stack),
stack_params.items())

def test_missing_params_no_stack(self):
params = {}
required = ['Address']
with self.assertRaises(MissingParameterException) as cm:
handle_missing_parameters(params, required)

self.assertEqual(cm.exception.parameters, required)

def test_stack_params_dont_override_given_params(self):
stack_params = {'Address': '10.0.0.1'}
stack = MockStack(stack_params)
def_params = {'Address': '192.168.0.1'}
required = ['Address']
result = handle_missing_parameters(def_params, required, stack)
self.assertEqual(result, def_params.items())

0 comments on commit 1671a90

Please sign in to comment.