diff --git a/docs/source/cfngin/config.rst b/docs/source/cfngin/config.rst index d28a0df20..85f022c8f 100644 --- a/docs/source/cfngin/config.rst +++ b/docs/source/cfngin/config.rst @@ -4,6 +4,7 @@ .. _Blueprints: ../terminology.html#blueprint .. _hook: ../terminology.html#hook .. _hooks: ../terminology.html#hook +.. _lookups: lookups.html .. _Mappings: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html .. _Outputs: ../terminology.html#output @@ -251,7 +252,10 @@ The keyword is a dictionary with the following keys: with a variable pulled from an environment file. **args:** - A dictionary of arguments to pass to the hook_. + A dictionary of arguments to pass to the hook_ with support for lookups_. + Note that lookups_ that change the order of execution, like ``output``, can + only be used in a `post` hook but hooks like ``rxref`` are able to be used + with either `pre` or `post` hooks. An example using the ``create_domain`` hook_ for creating a route53 domain before the build action: @@ -281,6 +285,33 @@ should run in the environment CFNgin is running against: args: domain: mydomain.com +An example of a custom hooks using various lookups in it's arguments: + +.. code-block:: yaml + + pre_build: + custom_hook1: + path: path.to.hook1.entry_point + args: + ami: ${ami [@]owners:self,888888888888,amazon name_regex:server[0-9]+ architecture:i386} + user_data: ${file parameterized-64:file://some/path} + db_endpoint: ${rxref some-stack::Endpoint} + subnet: ${xref some-stack::Subnet} + db_creds: ${ssmstore us-east-1@MyDBUser} + custom_hook2: + path: path.to.hook.entry_point + args: + bucket: ${dynamodb us-east-1:TestTable@TestKey:TestVal.BucketName} + bucket_region: ${envvar AWS_REGION} # this variable is set by Runway + files: + - ${file plain:file://some/path} + + post_build: + custom_hook3: + path: path.to.hook3.entry_point + args: + nlb: ${output nlb-stack::Nlb} # output can only be used as a post hook + Tags ---- diff --git a/runway/cfngin/hooks/utils.py b/runway/cfngin/hooks/utils.py index 5c54ae23d..fb5e78fb8 100644 --- a/runway/cfngin/hooks/utils.py +++ b/runway/cfngin/hooks/utils.py @@ -5,6 +5,9 @@ import sys from runway.util import load_object_from_string +from runway.variables import Variable, resolve_variables + +from ..exceptions import FailedVariableLookup LOGGER = logging.getLogger(__name__) @@ -43,12 +46,12 @@ def handle_hooks(stage, hooks, provider, context): for hook in hooks: data_key = hook.data_key required = hook.required - kwargs = hook.args or {} - enabled = hook.enabled - if not enabled: + + if not hook.enabled: LOGGER.debug("hook with method %s is disabled, skipping", hook.path) continue + try: method = load_object_from_string(hook.path) except (AttributeError, ImportError): @@ -56,6 +59,24 @@ def handle_hooks(stage, hooks, provider, context): if required: raise continue + + if isinstance(hook.args, dict): + args = [Variable(k, v) for k, v in hook.args.items()] + try: # handling for output or similar being used in pre_build + resolve_variables(args, context, provider) + except FailedVariableLookup: + if 'pre' in stage: + LOGGER.error('Lookups that change the order of ' + 'execution, like "output", can only be ' + 'used in "post_*" hooks. Please ' + 'ensure that the hook being used does ' + 'not rely on a stack, hook_data, or ' + 'context that does not exist yet.') + raise + kwargs = {v.name: v.value for v in args} + else: + kwargs = hook.args or {} + try: result = method(context=context, provider=provider, **kwargs) except Exception: # pylint: disable=broad-except @@ -63,6 +84,7 @@ def handle_hooks(stage, hooks, provider, context): if required: raise continue + if not result: if required: LOGGER.error("Required hook %s failed. Return value: %s", diff --git a/tests/cfngin/hooks/test_utils.py b/tests/cfngin/hooks/test_utils.py index 64dc6f4ff..7941848d0 100644 --- a/tests/cfngin/hooks/test_utils.py +++ b/tests/cfngin/hooks/test_utils.py @@ -142,6 +142,22 @@ def test_return_data_hook_duplicate_key(self): with self.assertRaises(KeyError): handle_hooks("result", hooks, "us-east-1", self.context) + def test_resolve_lookups_in_args(self): + """Test the resolution of lookups in hook args.""" + hooks = [Hook({ + "path": "tests.cfngin.hooks.test_utils.kwargs_hook", + "data_key": "my_hook_results", + "args": { + "default_lookup": "${default env_var::default_value}" + } + })] + handle_hooks("lookups", hooks, "us-east-1", self.context) + + self.assertEqual( + self.context.hook_data["my_hook_results"]["default_lookup"], + "default_value" + ) + def mock_hook(*args, **kwargs): """Mock hook.""" @@ -167,3 +183,8 @@ def context_hook(*args, **kwargs): def result_hook(*args, **kwargs): """Results hook.""" return {"foo": "bar"} + + +def kwargs_hook(*args, **kwargs): + """Kwargs hook.""" + return kwargs