Hooks
A hook
is a python function, class, or class method that is executed before or after an action is taken for the entire config.
Only the following actions allow pre/post hooks:
- deploy
using fields
pre_deploy
andpost_deploy
- destroy
using fields
pre_destroy
andpost_destroy
- class cfngin.hook
When defining a hook in one of the supported fields, the follow fields can be used.
Lookup Support
The following fields support lookups:
- args: Optional[Dict[str, Any]] = {}
A dictionary of arguments to pass to the hook.
This field supports the use of lookups.
Important
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.
Example
pre_deploy: - args: key: ${val}
- data_key: Optional[str] = None
If set, and the hook returns data (a dictionary or
pydantic.BaseModel
), the results will be stored inCfnginContext.hook_data
with thedata_key
as its key.Example
pre_deploy: - data_key: example-key
- enabled: Optional[bool] = True
Whether to execute the hook every CFNgin run. This field provides the ability to execute a hook per environment when combined with a variable.
Example
pre_deploy: - enabled: ${enable_example_hook}
Built-in Hooks
- acm.Certificate
- aws_lambda.upload_lambda_functions
- awslambda.PythonFunction
- awslambda.PythonLayer
- build_staticsite.build
- cleanup_s3.purge_bucket
- cleanup_ssm.delete_param
- command.run_command
- docker.image.build
- docker.image.push
- docker.image.remove
- docker.login
- ecr.purge_repository
- ecs.create_clusters
- iam.create_ecs_service_role
- iam.ensure_server_cert_exists
- keypair.ensure_keypair_exists
- route53.create_domain
- ssm.parameter.SecureString
- upload_staticsite.sync
Writing A Custom Hook
A custom hook must be in an executable, importable python package or standalone file.
The hook must be importable using your current sys.path
.
This takes into account the sys_path
defined in the config
file as well as any paths
of package_sources
.
When executed, the hook will have various keyword arguments passed to it.
The keyword arguments that will always be passed to the hook are context
(CfnginContext
) and provider
(Provider
).
Anything defined in the args
field will also be passed to hook as a keyword argument.
For this reason, it is recommended to use an unpack operator (**kwargs
) in addition to the keyword arguments the hook requires to ensure future compatibility and account for misconfigurations.
The hook must return True
or a truthy object if it was successful.
It must return False
or a falsy object if it failed.
This signifies to CFNgin whether or not to halt execution if the hook is required
.
If a Dict
, MutableMap
, or pydantic.BaseModel
is returned, it can be accessed by subsequent hooks, lookups, or Blueprints from the context object.
It will be stored as context.hook_data[data_key]
where data_key
is the value set in the hook definition.
If data_key
is not provided or the type of the returned data is not a Dict
, MutableMap
, or pydantic.BaseModel
, it will not be added to the context object.
Important
When using a pydantic.root_validator()
or pydantic.validator()
allow_reuse=True
must be passed to the decorator.
This is because of how hooks are loaded/re-loaded for each usage.
Failure to do so will result in an error if the hook is used more than once.
If using boto3 in a hook, use context.get_session()
instead of creating a new session to ensure the correct credentials are used.
"""context.get_session() example."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from runway.context import CfnginContext
def do_something(context: CfnginContext, **_kwargs: Any) -> None:
"""Do something."""
s3_client = context.get_session().client("s3")
Example Hook Function
"""My hook."""
from typing import Dict, Optional
def do_something(
*, is_failure: bool = True, name: str = "Kevin", **_kwargs: str
) -> Optional[Dict[str, str]]:
"""Do something."""
if is_failure:
return None
return {"result": f"You are not a failure {name}."}
namespace: example
sys_path: ./
pre_deploy:
- path: hooks.my_hook.do_something
args:
is_failure: false
Example Hook Class
Hook classes must implement the interface detailed by the CfnginHookProtocol
Protocol
.
This can be done implicitly or explicitly (by creating a subclass of CfnginHookProtocol
).
As shown in this example, HookArgsBaseModel
or it’s parent class BaseModel
can be used to create self validating and sanitizing data models.
These can then be used to parse the values provided in the args
field to ensure they match what is expected.
"""My hook."""
import logging
from typing import TYPE_CHECKING, Any, Dict, Optional
from runway.utils import BaseModel
from runway.cfngin.hooks.protocols import CfnginHookProtocol
if TYPE_CHECKING:
from ...context import CfnginContext
LOGGER = logging.getLogger(__name__)
class MyClassArgs(BaseModel):
"""Arguments for MyClass hook.
Attributes:
is_failure: Force the hook to fail if true.
name: Name used in the response.
"""
is_failure: bool = False
name: str
class MyClass(CfnginHookProtocol):
"""My class does a thing.
Keyword Args:
is_failure (bool): Force the hook to fail if true.
name (str): Name used in the response.
Returns:
Dict[str, str]: Response message is stored in ``result``.
Example:
.. code-block:: yaml
pre_deploy:
- path: hooks.my_hook.MyClass
args:
is_failure: False
name: Karen
"""
args: MyClassArgs
def __init__(self, context: CfnginContext, **kwargs: Any) -> None:
"""Instantiate class.
Args:
context: Context instance. (passed in by CFNgin)
provider: Provider instance. (passed in by CFNgin)
"""
kwargs.setdefault("tags", {})
self.args = self.ARGS_PARSER.parse_obj(kwargs)
self.args.tags.update(context.tags)
self.context = context
def post_deploy(self) -> Optional[Dict[str, str]]:
"""Run during the **post_deploy** stage."""
if self.args["is_failure"]:
return None
return {"result": f"You are not a failure {self.args['name']}."}
def post_destroy(self) -> None:
"""Run during the **post_destroy** stage."""
LOGGER.error("post_destroy is not supported by this hook")
def pre_deploy(self) -> None:
"""Run during the **pre_deploy** stage."""
LOGGER.error("pre_deploy is not supported by this hook")
def pre_destroy(self) -> None:
"""Run during the **pre_destroy** stage."""
LOGGER.error("pre_destroy is not supported by this hook")
namespace: example
sys_path: ./
pre_deploy:
- path: hooks.my_hook.MyClass
args:
is_failure: False
name: Karen