Skip to content

Commit

Permalink
Merge pull request #10 from brettswift/feature/required_params
Browse files Browse the repository at this point in the history
Support auto param generation logic, validation
  • Loading branch information
brettswift committed Sep 20, 2018
2 parents 6d57f49 + c1a27a9 commit 966d52f
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 18 deletions.
38 changes: 38 additions & 0 deletions cumulus/chain/chain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import troposphere # noqa
from termcolor import colored

from cumulus.chain import chaincontext # noqa
from cumulus.chain.params import TemplateRequirements # noqa
from cumulus.util.tropo import TemplateQuery # noqa


class Chain:

Expand All @@ -18,6 +23,39 @@ def run(self, chain_context):
:type chain_context: chaincontext.ChainContext
"""
self._execute_all_steps(chain_context)
self.validate_template(chain_context)

def validate_template(self, chain_context):
"""
:type chain_context: chaincontext.ChainContext
"""

required_params = chain_context.required_params # type: TemplateRequirements

template_params = chain_context.template.parameters

# Remove all parameters that are already supplied in the template.
for template_param in template_params:
if required_params.params.__contains__(template_param):
required_params.params.remove(template_param)
print("Required parameter '%s' was satisfied" % template_param)

unsatisfied_params = ""
# Build an output string of the remaining params not satisfied
for param in required_params.params:
required_line = "Template Requires a parameter named: %s " % param
unsatisfied_params += required_line + "\n"
print(colored(required_line, color='red'))

# If we have params left over, validation failed.
if len(required_params.params):
message = ("Template is invalid. Exiting"
"all the required params are: ")
raise AssertionError(message + "\n" + unsatisfied_params)

def _execute_all_steps(self, chain_context):
for step in self._steps:
print(colored("RUNNING STEP for class %s " % step.__class__, color='yellow'))
step.handle(chain_context)
14 changes: 14 additions & 0 deletions cumulus/chain/chaincontext.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from cumulus.chain.params import TemplateRequirements


class ChainContext:

def __init__(self,
template,
instance_name,
auto_param_creation=True
):
"""
:type template: troposphere.Template
"""
self._auto_param_creation = auto_param_creation
self._instance_name = instance_name
self._template = template
self._metadata = {}
self._required_params = TemplateRequirements()

@property
def template(self):
Expand All @@ -31,3 +37,11 @@ def metadata(self):
def instance_name(self):
# TODO: validate instance name for s3 compatibility (cuz it could be used there)
return self._instance_name

@property
def required_params(self):
return self._required_params

@property
def auto_param_creation(self):
return self._auto_param_creation
12 changes: 12 additions & 0 deletions cumulus/chain/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

class TemplateRequirements:

def __init__(self):
self._required_params = []

@property
def params(self):
return self._required_params

def add(self, param):
self._required_params.append(param)
2 changes: 1 addition & 1 deletion cumulus/steps/dev_tools/code_build_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(self,
def handle(self, chain_context):

print("Adding action %s Stage." % self.action_name)
suffix = "%s%s" %(self.stage_name_to_add, self.action_name)
suffix = "%s%s" % (self.stage_name_to_add, self.action_name)

policy_name = "CodeBuildPolicy%s" % chain_context.instance_name
role_name = "CodeBuildRole%s" % suffix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import cumulus.policies.codebuild
from cumulus.chain import step
from cumulus.steps.dev_tools import META_PIPELINE_BUCKET_POLICY_REF
from cumulus.types.codebuild.buildaction import SourceS3Action, SourceCodeCommitAction
from cumulus.types.codebuild.buildaction import SourceCodeCommitAction
from cumulus.util.tropo import TemplateQuery


Expand Down
9 changes: 7 additions & 2 deletions cumulus/util/tropo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ def get_resource_by_type(template, type_to_find):
if item.__class__ is type_to_find:
result.append(item)

if len(result) == 0:
raise ValueError("Expected to find resource of type %s in template but did not." % type_to_find)
# TODO: review: how does this affect other behaviours..
# if len(result) == 0:
# raise ValueError("Expected to find resource of type %s in template but did not." % type_to_find)
return result

@staticmethod
Expand All @@ -38,6 +39,10 @@ def get_pipeline_stage_by_name(template, stage_name):
found_pipelines = TemplateQuery.get_resource_by_type(
template=template,
type_to_find=codepipeline.Pipeline)

if not found_pipelines:
raise ValueError("Expected to find a pipeline in the template, but did not.")

pipeline = found_pipelines[0]
# Alternate way to get this
# pipeline = TemplateQuery.get_resource_by_title(chain_context.template, 'AppPipeline')
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
'pytest-watch',
'pytest-cov',
'coveralls',
'awscli'
'awscli',
'mock'
]

extras = {
Expand Down
120 changes: 120 additions & 0 deletions tests/unit/chain/test_required_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
try:
# python 3
from unittest.mock import patch # noqa
from unittest.mock import MagicMock
except: # noqa
# python 2
from mock import patch, MagicMock # noqa

import unittest

import troposphere

from cumulus.chain import chaincontext, step
from cumulus.chain import chain


class MockStepWithRequiredParam(step.Step):

def handle(self, chain_context):
"""
A sample handle event, showing how parameters should be implemented.
:type chain_context: chaincontext.ChainContext
"""
if not chain_context.auto_param_creation:
chain_context.required_params.add("NumberOfMinions")
chain_context.required_params.add("NumberOfEyeballs")

if chain_context.auto_param_creation:
chain_context.template.add_parameter(troposphere.Parameter("NumberOfMinions", Type="String"))
chain_context.template.add_parameter(troposphere.Parameter("NumberOfEyeballs", Type="String"))


class TestRequiredParams(unittest.TestCase):

def setUp(self):
self.context = chaincontext.ChainContext(
template=troposphere.Template(),
instance_name='justtestin',
auto_param_creation=False
)

def tearDown(self):
del self.context

def test_should_call_validate_when_chain_runs(self):
the_chain = chain.Chain()

the_chain.validate_template = MagicMock
the_chain.run(self.context)
self.assertTrue(the_chain.validate_template.called)

def test_should_throw_error_with_unsatisfied_required_params(self):
the_chain = chain.Chain()
mock_step = MockStepWithRequiredParam()
the_chain.add(mock_step)

self.assertRaisesRegexp(
AssertionError,
'.*Minions.*\n.*Eyeballs.*',
the_chain.run,
self.context
)

def test_should_validate_template_with_missing_params(self):
"""
This tests the same functionality as test_should_throw_error_with_unsatisfied_required_params
however, it doesn't use the example code. The idea was to keep example code even though
it's less of a unit test.. to show how you would use params. Tests as documentation :)
:return:
"""
the_chain = chain.Chain()

self.context.required_params.add("ImARequiredParam")

self.assertRaisesRegexp(
AssertionError,
'ImARequiredParam',
the_chain.validate_template,
self.context
)

def test_should_validate_template_without_any_required_params(self):
the_chain = chain.Chain()
the_chain.run(self.context)

def test_should_build_template_with_required_parameters_added_externally(self):
the_chain = chain.Chain()
mock_step = MockStepWithRequiredParam()
the_chain.add(mock_step)

self.context = chaincontext.ChainContext(
template=troposphere.Template(),
instance_name='wont_generate_parameters',
auto_param_creation=False
)

self.context.template.add_parameter(troposphere.Parameter(
"NumberOfMinions",
Type="String"
))

self.context.template.add_parameter(troposphere.Parameter(
"NumberOfEyeballs",
Type="String"
))

the_chain.run(self.context)

def test_should_build_template_with_required_parameters_added_automatically(self):
the_chain = chain.Chain()
mock_step = MockStepWithRequiredParam()
the_chain.add(mock_step)

self.context = chaincontext.ChainContext(
template=troposphere.Template(),
instance_name='will_generate_parameters',
auto_param_creation=True
)

the_chain.run(self.context)
15 changes: 2 additions & 13 deletions tests/unit/util/test_tropo.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,6 @@ def test_should_not_find_resource_by_type(self):
"thebucket"
))

self.assertRaises(
ValueError,
TemplateQuery.get_resource_by_type,
template=t,
type_to_find=troposphere.s3.Policy
)
results = TemplateQuery.get_resource_by_type(t, troposphere.s3.Policy)

self.assertRaisesRegexp(
ValueError,
"Expected to find.+of type.+Policy",
callable_obj=TemplateQuery.get_resource_by_type,
template=t,
type_to_find=troposphere.s3.Policy
)
self.assertTrue(results.count(results) == 0)

0 comments on commit 966d52f

Please sign in to comment.