Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion docs/source/cfngin/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 [<region>@]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
----
Expand Down
28 changes: 25 additions & 3 deletions runway/cfngin/hooks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -43,26 +46,45 @@ 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):
LOGGER.exception("Unable to load method at %s:", hook.path)
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
LOGGER.exception("Method %s threw an exception:", hook.path)
if required:
raise
continue

if not result:
if required:
LOGGER.error("Required hook %s failed. Return value: %s",
Expand Down
21 changes: 21 additions & 0 deletions tests/cfngin/hooks/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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