diff --git a/pom.xml b/pom.xml
index 4b866b5c..7f91f9f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
software.amazon.cloudformation
aws-cloudformation-rpdk-java-plugin
- 2.0.11
+ 2.0.12
AWS CloudFormation RPDK Java Plugin
The CloudFormation Resource Provider Development Kit (RPDK) allows you to author your own resource providers that can be used by CloudFormation. This plugin library helps to provide runtime bindings for the execution of your providers by CloudFormation.
@@ -118,13 +118,43 @@
com.amazonaws
aws-java-sdk-core
- 1.11.555
+ 1.11.1034
com.amazonaws
aws-lambda-java-core
- 1.2.0
+ 1.2.1
+
+
+
+ com.amazonaws
+ aws-lambda-java-log4j2
+ 1.5.1
+
+
+
+ com.amazonaws
+ aws-encryption-sdk-java
+ 1.7.0
+
+
+
+ com.amazonaws
+ aws-java-sdk-sts
+ 1.11.1034
+
+
+
+ com.google.code.findbugs
+ annotations
+ 3.0.0
+
+
+
+ com.amazonaws
+ aws-java-sdk-kms
+ 1.11.1034
@@ -173,6 +203,12 @@
commons-lang3
3.9
+
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
com.google.guava
diff --git a/python/rpdk/java/__init__.py b/python/rpdk/java/__init__.py
index a5ba28b2..1ffa6332 100644
--- a/python/rpdk/java/__init__.py
+++ b/python/rpdk/java/__init__.py
@@ -1,5 +1,5 @@
import logging
-__version__ = "2.0.11"
+__version__ = "2.0.12"
logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/python/rpdk/java/codegen.py b/python/rpdk/java/codegen.py
index a700d238..8584cb10 100644
--- a/python/rpdk/java/codegen.py
+++ b/python/rpdk/java/codegen.py
@@ -1,5 +1,6 @@
# pylint: disable=useless-super-delegation,too-many-locals
# pylint doesn't recognize abstract methods
+import json
import logging
import os
import shutil
@@ -9,17 +10,19 @@
from rpdk.core.data_loaders import resource_stream
from rpdk.core.exceptions import InternalError, SysExitRecommendedError
-from rpdk.core.init import input_with_validation
-from rpdk.core.jsonutils.resolver import resolve_models
+from rpdk.core.jsonutils.resolver import ContainerType, ResolvedType, resolve_models
from rpdk.core.plugin_base import LanguagePlugin
+from rpdk.core.project import ARTIFACT_TYPE_HOOK
+from rpdk.core.utils.init_utils import input_with_validation
from . import __version__
-from .resolver import translate_type
+from .resolver import UNDEFINED, translate_type
from .utils import safe_reserved, validate_codegen_model, validate_namespace
LOG = logging.getLogger(__name__)
-OPERATIONS = ("Create", "Read", "Update", "Delete", "List")
+HOOK_OPERATIONS = ("PreCreate", "PreUpdate", "PreDelete")
+RESOURCE_OPERATIONS = ("Create", "Read", "Update", "Delete", "List")
EXECUTABLE = "cfn"
AWSCODEGEN = namedtuple("AWSCODEGEN", "default guided default_code guided_code")
CODEGEN = AWSCODEGEN("default", "guided_aws", "1", "2")
@@ -68,9 +71,12 @@ class InvalidMavenPOMError(SysExitRecommendedError):
class JavaLanguagePlugin(LanguagePlugin):
MODULE_NAME = __name__
RUNTIME = "java8"
- ENTRY_POINT = "{}.HandlerWrapper::handleRequest"
- TEST_ENTRY_POINT = "{}.HandlerWrapper::testEntrypoint"
- EXECUTABLE_ENTRY_POINT = "{}.HandlerWrapperExecutable"
+ HOOK_ENTRY_POINT = "{}.HookHandlerWrapper::handleRequest"
+ HOOK_TEST_ENTRY_POINT = "{}.HookHandlerWrapper::testEntrypoint"
+ HOOK_EXECUTABLE_ENTRY_POINT = "{}.HookHandlerWrapperExecutable"
+ RESOURCE_ENTRY_POINT = "{}.HandlerWrapper::handleRequest"
+ RESOURCE_TEST_ENTRY_POINT = "{}.HandlerWrapper::testEntrypoint"
+ RESOURCE_EXECUTABLE_ENTRY_POINT = "{}.HandlerWrapperExecutable"
CODE_URI = "./target/{}-1.0-SNAPSHOT.jar"
def __init__(self):
@@ -143,6 +149,7 @@ def _writing_component(
operation: str = kwargs.get("operation")
pojo_name: str = kwargs.get("pojo_name")
call_graph: str = kwargs.get("call_graph")
+ target_names: list = kwargs.get("target_names")
if not stub_entity:
stub_entity = entity
@@ -154,6 +161,7 @@ def _writing_component(
operation=operation,
pojo_name=pojo_name,
call_graph=call_graph,
+ target_names=target_names,
)
project.safewrite(path, contents)
@@ -222,11 +230,18 @@ def init_shared(self, project, src, tst, resources):
path = src / "Configuration.java"
LOG.debug("Writing configuration: %s", path)
- template = self.env.get_template("init/shared/StubConfiguration.java")
+ config_template = (
+ "init/shared/StubHookConfiguration.java"
+ if project.artifact_type == ARTIFACT_TYPE_HOOK
+ else "init/shared/StubConfiguration.java"
+ )
+ template = self.env.get_template(config_template)
contents = template.render(
package_name=self.package_name,
schema_file_name=project.schema_filename,
- pojo_name="ResourceModel",
+ pojo_name="HookInputModel"
+ if project.artifact_type == ARTIFACT_TYPE_HOOK
+ else "ResourceModel",
)
project.safewrite(path, contents)
@@ -234,7 +249,9 @@ def init_shared(self, project, src, tst, resources):
path = project.root / "template.yml"
LOG.debug("Writing SAM template: %s", path)
template = self.env.get_template(
- "template.yml"
+ "template_hook.yml"
+ if project.artifact_type == ARTIFACT_TYPE_HOOK
+ else "template.yml"
) # this template is in the CLI itself
handler_params = {
"Handler": project.entrypoint,
@@ -256,7 +273,12 @@ def init_shared(self, project, src, tst, resources):
# generate docs
path = project.root / "README.md"
LOG.debug("Writing README: %s", path)
- template = self.env.get_template("init/shared/README.md")
+ doc_template = (
+ "init/shared/README_HOOK.md"
+ if project.artifact_type == ARTIFACT_TYPE_HOOK
+ else "init/shared/README_RESOURCE.md"
+ )
+ template = self.env.get_template(doc_template)
contents = template.render(
type_name=project.type_name,
schema_path=project.schema_path,
@@ -270,28 +292,78 @@ def init_shared(self, project, src, tst, resources):
contents = resource_stream(__name__, "data/log4j2.xml").read()
project.safewrite(path, contents)
- self.init_handlers(project, src, tst)
+ if project.artifact_type == ARTIFACT_TYPE_HOOK:
+ self.init_hook_handlers(project, src, tst)
+ else:
+ self.init_resource_handlers(project, src, tst)
@logdebug
def init_guided_aws(self, project, src, tst):
"""Writing supporting modules"""
- self._writing_component(project, src, entity="Translator.java")
- self._writing_component(project, src, entity="ClientBuilder.java")
- self._writing_component(project, src, entity="BaseHandlerStd.java")
- self._writing_component(
- project,
- src,
- entity="TagHelper.java",
- operation="TagOps",
- call_graph=project.type_name.replace("::", "-"),
- )
- self._writing_component(project, tst, entity="AbstractTestBase.java")
+ if project.artifact_type == ARTIFACT_TYPE_HOOK:
+ self._writing_component(
+ project,
+ src,
+ entity="Translator.java",
+ stub_entity="HookTranslator.java",
+ )
+ self._writing_component(project, src, entity="ClientBuilder.java")
+ self._writing_component(project, src, entity="BaseHookHandlerStd.java")
+ self._writing_component(project, tst, entity="AbstractTestBase.java")
+ else:
+ self._writing_component(project, src, entity="Translator.java")
+ self._writing_component(project, src, entity="ClientBuilder.java")
+ self._writing_component(project, src, entity="BaseHandlerStd.java")
+ self._writing_component(
+ project,
+ src,
+ entity="TagHelper.java",
+ operation="TagOps",
+ call_graph=project.type_name.replace("::", "-"),
+ )
+ self._writing_component(project, tst, entity="AbstractTestBase.java")
@logdebug
- def init_handlers(self, project, src, tst):
+ def init_hook_handlers(self, project, src, tst):
+ """Writing hook stub handlers and tests"""
+ handlers = project.schema.get("handlers")
+ for operation in HOOK_OPERATIONS:
+ entity = "{}HookHandler.java".format(operation)
+ entity_test = "{}HookHandlerTest.java".format(operation)
+
+ stub_entity = "Stub{}HookHandler.java".format(
+ operation if self._is_aws_guided(project) else ""
+ )
+ stub_entity_test = "Stub{}HookHandlerTest.java".format(
+ operation if self._is_aws_guided(project) else ""
+ )
+ target_names = handlers.get(operation[0].lower() + operation[1:], {}).get(
+ "targetNames", ["My::Example::Resource"]
+ )
+
+ self._writing_component(
+ project,
+ src,
+ entity=entity,
+ stub_entity=stub_entity,
+ operation=operation,
+ call_graph=project.type_name.replace("::", "-"),
+ target_names=target_names,
+ )
+ self._writing_component(
+ project,
+ tst,
+ entity=entity_test,
+ stub_entity=stub_entity_test,
+ operation=operation,
+ target_names=target_names,
+ )
+
+ @logdebug
+ def init_resource_handlers(self, project, src, tst):
"""Writing stub handlers and tests"""
pojo_name = "ResourceModel"
- for operation in OPERATIONS:
+ for operation in RESOURCE_OPERATIONS:
entity = "{}Handler.java".format(operation)
entity_test = "{}HandlerTest.java".format(operation)
@@ -322,11 +394,22 @@ def init_handlers(self, project, src, tst):
def _init_settings(self, project):
project.runtime = self.RUNTIME
- project.entrypoint = self.ENTRY_POINT.format(self.package_name)
- project.test_entrypoint = self.TEST_ENTRY_POINT.format(self.package_name)
- project.executable_entrypoint = self.EXECUTABLE_ENTRY_POINT.format(
- self.package_name
- )
+ if project.artifact_type == ARTIFACT_TYPE_HOOK:
+ project.entrypoint = self.HOOK_ENTRY_POINT.format(self.package_name)
+ project.test_entrypoint = self.HOOK_TEST_ENTRY_POINT.format(
+ self.package_name
+ )
+ project.executable_entrypoint = self.HOOK_EXECUTABLE_ENTRY_POINT.format(
+ self.package_name
+ )
+ else:
+ project.entrypoint = self.RESOURCE_ENTRY_POINT.format(self.package_name)
+ project.test_entrypoint = self.RESOURCE_TEST_ENTRY_POINT.format(
+ self.package_name
+ )
+ project.executable_entrypoint = self.RESOURCE_EXECUTABLE_ENTRY_POINT.format(
+ self.package_name
+ )
project.settings.update(DEFAULT_SETTINGS)
@staticmethod
@@ -360,7 +443,14 @@ def generate(self, project):
LOG.debug("Making generated tests folder structure: %s", test_src)
test_src.mkdir(parents=True, exist_ok=True)
- # write generated handler integration with LambdaWrapper
+ if project.artifact_type == ARTIFACT_TYPE_HOOK:
+ self.generate_hook(src, project)
+ else:
+ self.generate_resource(src, project)
+
+ @logdebug
+ def generate_resource(self, src, project):
+ # write generated resource handler integration with LambdaWrapper
path = src / "HandlerWrapper.java"
LOG.debug("Writing handler wrapper: %s", path)
template = self.env.get_template("generate/HandlerWrapper.java")
@@ -391,7 +481,7 @@ def generate(self, project):
template = self.env.get_template("generate/BaseHandler.java")
contents = template.render(
package_name=self.package_name,
- operations=OPERATIONS,
+ operations=RESOURCE_OPERATIONS,
contains_type_configuration=project.configuration_schema,
pojo_name="ResourceModel",
)
@@ -447,6 +537,188 @@ def generate(self, project):
LOG.debug("Generate complete")
+ @logdebug
+ def generate_hook(self, src, project): # pylint: disable=too-many-statements
+ # write generated hook handler integration with HookLambdaWrapper
+ path = src / "HookHandlerWrapper.java"
+ LOG.debug("Writing hook handler wrapper: %s", path)
+ template = self.env.get_template("generate/hook/HookHandlerWrapper.java")
+ contents = template.render(
+ package_name=self.package_name,
+ operations=project.schema.get("handlers", {}).keys(),
+ wrapper_parent="HookLambdaWrapper",
+ )
+ project.overwrite(path, contents)
+
+ # write generated handler integration with HookExecutableWrapper
+ self._write_executable_wrapper_class(src, project)
+
+ path = src / "BaseHookHandler.java"
+ LOG.debug("Writing base hook handler: %s", path)
+ template = self.env.get_template("generate/hook/BaseHookHandler.java")
+ contents = template.render(
+ package_name=self.package_name, operations=HOOK_OPERATIONS
+ )
+ project.overwrite(path, contents)
+
+ # generate POJOs
+ models = resolve_models(project.schema, "HookInputModel")
+ if project.configuration_schema:
+ configuration_schema_path = (
+ self._get_generated_root(project)
+ / project.configuration_schema_filename
+ )
+ project.write_configuration_schema(configuration_schema_path)
+ configuration_models = resolve_models(
+ project.configuration_schema, "TypeConfigurationModel"
+ )
+ else:
+ configuration_models = {"TypeConfigurationModel": {}}
+ models.update(configuration_models)
+
+ LOG.debug("Writing %d POJOs", len(models))
+
+ base_template = self.env.get_template("generate/hook/HookInputModel.java")
+ pojo_template = self.env.get_template("generate/POJO.java")
+
+ for model_name, properties in models.items():
+ path = src / "{}.java".format(model_name)
+ LOG.debug("%s POJO: %s", model_name, path)
+
+ if model_name == "HookInputModel": # pragma: no cover
+ contents = base_template.render(
+ type_name=project.type_name,
+ package_name=self.package_name,
+ model_name=model_name,
+ properties=properties,
+ )
+ else:
+ contents = pojo_template.render(
+ package_name=self.package_name,
+ model_name=model_name,
+ properties=properties,
+ no_args_constructor_required=(
+ model_name != "TypeConfigurationModel" or len(properties) != 0
+ ),
+ )
+ project.overwrite(path, contents)
+
+ loaded_target_schema_file_names = {}
+ for target_type_name, target_info in project.target_info.items():
+ target_schema = target_info["Schema"]
+
+ target_namespace = [
+ s.lower() for s in target_type_name.split("::")
+ ] # AWS::SQS::Queue -> awssqsqueue
+ target_name = "".join(
+ [s.capitalize() for s in target_namespace]
+ ) # awssqsqueue -> AwsSqsQueue
+ target_schema_file_name = "{}.json".format(
+ "-".join(target_namespace)
+ ) # awssqsqueue -> aws-sqs-queue.json
+ target_model_package_name = "{}.model.{}".format(
+ self.package_name, ".".join(target_namespace)
+ )
+ target_model_dir = (src / "model").joinpath(*target_namespace)
+ target_model_dir.mkdir(parents=True, exist_ok=True)
+
+ loaded_target_schemas_dir = (
+ project.root / "target" / "loaded-target-schemas"
+ )
+ loaded_target_schemas_dir.mkdir(parents=True, exist_ok=True)
+ if target_info.get("SchemaFileAvailable"): # pragma: no cover
+ contents = json.dumps(target_schema, indent=4)
+ path = loaded_target_schemas_dir / target_schema_file_name
+ project.overwrite(path, contents)
+ loaded_target_schema_file_names[
+ target_type_name
+ ] = target_schema_file_name
+
+ is_registry_type = target_info.get("IsCfnRegistrySupportedType")
+
+ if not target_schema:
+ target_schema = {
+ "typeName": target_name,
+ "properties": {},
+ "required": [],
+ "additionalProperties": False,
+ }
+
+ # generate POJOs
+ models = resolve_models(target_schema, target_name)
+
+ # TODO: Remove once tagging is fully supported
+ if models.get(target_name, {}).get("Tags"): # pragma: no cover
+ models[target_name]["Tags"] = ResolvedType(
+ ContainerType.PRIMITIVE, UNDEFINED
+ )
+
+ LOG.debug("Writing %d POJOs", len(models))
+
+ base_template = self.env.get_template(
+ "generate/hook/ResourceHookTargetModel.java"
+ )
+ target_template = self.env.get_template(
+ "generate/hook/ResourceHookTarget.java"
+ )
+ pojo_template = self.env.get_template("generate/POJO.java")
+
+ for model_name, properties in models.items():
+ path = target_model_dir / "{}.java".format(model_name)
+ LOG.debug("%s POJO: %s", model_name, path)
+
+ if model_name == target_name:
+ contents = target_template.render(
+ type_name=target_type_name,
+ package_name=target_model_package_name,
+ model_name=target_name,
+ is_registry_type=is_registry_type,
+ schema_available=(
+ target_info.get("SchemaFileAvailable", False)
+ ),
+ schema_path=loaded_target_schema_file_names.get(
+ target_type_name
+ ),
+ properties=properties,
+ primaryIdentifier=target_schema.get("primaryIdentifier", []),
+ additionalIdentifiers=target_schema.get(
+ "additionalIdentifiers", []
+ ),
+ )
+ else:
+ contents = pojo_template.render(
+ package_name=target_model_package_name,
+ model_name=model_name,
+ properties=properties,
+ no_args_constructor_required=(
+ model_name != "TypeConfigurationModel"
+ or len(properties) != 0
+ ),
+ )
+ project.overwrite(path, contents)
+
+ path = target_model_dir / "{}TargetModel.java".format(target_name)
+ contents = base_template.render(
+ type_name=target_type_name,
+ model_name=target_name,
+ package_name=target_model_package_name,
+ )
+ project.overwrite(path, contents)
+
+ path = src / "BaseHookConfiguration.java"
+ LOG.debug("Writing base hook configuration: %s", path)
+ template = self.env.get_template("generate/hook/BaseHookConfiguration.java")
+ contents = template.render(
+ package_name=self.package_name,
+ schema_file_name=project.schema_filename,
+ target_schema_paths=loaded_target_schema_file_names,
+ )
+ project.overwrite(path, contents)
+
+ self._update_settings(project)
+
+ LOG.debug("Generate complete")
+
def _write_executable_wrapper_class(self, src, project):
try:
java_plugin_dependency_version = self._get_java_plugin_dependency_version(
@@ -456,16 +728,28 @@ def _write_executable_wrapper_class(self, src, project):
java_plugin_dependency_version
>= MINIMUM_JAVA_DEPENDENCY_VERSION_EXECUTABLE_HANDLER_WRAPPER
):
- path = src / "HandlerWrapperExecutable.java"
- LOG.debug("Writing handler wrapper: %s", path)
- template = self.env.get_template("generate/HandlerWrapper.java")
- contents = template.render(
- package_name=self.package_name,
- operations=project.schema.get("handlers", {}).keys(),
- pojo_name="ResourceModel",
- contains_type_configuration=project.configuration_schema,
- wrapper_parent="ExecutableWrapper",
- )
+ if project.artifact_type == ARTIFACT_TYPE_HOOK:
+ path = src / "HookHandlerWrapperExecutable.java"
+ LOG.debug("Writing handler wrapper: %s", path)
+ template = self.env.get_template(
+ "generate/hook/HookHandlerWrapper.java"
+ )
+ contents = template.render(
+ package_name=self.package_name,
+ operations=project.schema.get("handlers", {}).keys(),
+ wrapper_parent="HookExecutableWrapper",
+ )
+ else:
+ path = src / "HandlerWrapperExecutable.java"
+ LOG.debug("Writing handler wrapper: %s", path)
+ template = self.env.get_template("generate/HandlerWrapper.java")
+ contents = template.render(
+ package_name=self.package_name,
+ operations=project.schema.get("handlers", {}).keys(),
+ pojo_name="ResourceModel",
+ contains_type_configuration=project.configuration_schema,
+ wrapper_parent="ExecutableWrapper",
+ )
project.overwrite(path, contents)
else:
LOG.info(
@@ -509,9 +793,14 @@ def _update_settings(self, project):
hasattr(project, "executable_entrypoint")
and not project.executable_entrypoint
):
- project.executable_entrypoint = self.EXECUTABLE_ENTRY_POINT.format(
- self.package_name
- )
+ if project.artifact_type == ARTIFACT_TYPE_HOOK:
+ project.executable_entrypoint = self.HOOK_EXECUTABLE_ENTRY_POINT.format(
+ self.package_name
+ )
+ else:
+ project.executable_entrypoint = (
+ self.RESOURCE_EXECUTABLE_ENTRY_POINT.format(self.package_name)
+ )
project.write_settings()
@staticmethod
diff --git a/python/rpdk/java/templates/generate/hook/BaseHookConfiguration.java b/python/rpdk/java/templates/generate/hook/BaseHookConfiguration.java
new file mode 100644
index 00000000..ad960492
--- /dev/null
+++ b/python/rpdk/java/templates/generate/hook/BaseHookConfiguration.java
@@ -0,0 +1,60 @@
+// This is a generated file. Modifications will be overwritten.
+package {{ package_name }};
+
+import com.google.common.collect.ImmutableMap;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+@Data
+public abstract class BaseHookConfiguration {
+
+ private final Map targetSchemaPaths = new HashMap<>();
+ private final Map targetSchemas = new HashMap<>();
+ protected final String schemaFilename;
+
+ public BaseHookConfiguration(final String schemaFilename) {
+ this.schemaFilename = schemaFilename;
+ initialiseTargetSchemaFilePaths();
+ }
+
+ private void initialiseTargetSchemaFilePaths() {
+ {% for target_name, schema_path in target_schema_paths.items() %}
+ this.targetSchemaPaths.put("{{target_name}}", "{{schema_path}}");
+ {% endfor %}
+ }
+
+ public JSONObject hookSchemaJSONObject() {
+ return new JSONObject(new JSONTokener(this.getClass().getClassLoader().getResourceAsStream(schemaFilename)));
+ }
+
+ public JSONObject targetSchemaJSONObject(final String targetName) {
+ if (!this.targetSchemaPaths.containsKey(targetName)) {
+ return null;
+ }
+
+ return new JSONObject(
+ new JSONTokener(
+ this.targetSchemas.computeIfAbsent(
+ targetName,
+ tn -> this.getClass().getClassLoader().getResourceAsStream(tn)
+ )
+ )
+ );
+ }
+
+ public Map getTargetSchemaPaths() {
+ return ImmutableMap.copyOf(this.targetSchemaPaths);
+ }
+
+ public Map getTargetSchemas() {
+ return ImmutableMap.copyOf(this.targetSchemas);
+ }
+}
diff --git a/python/rpdk/java/templates/generate/hook/BaseHookHandler.java b/python/rpdk/java/templates/generate/hook/BaseHookHandler.java
new file mode 100644
index 00000000..e4d725b8
--- /dev/null
+++ b/python/rpdk/java/templates/generate/hook/BaseHookHandler.java
@@ -0,0 +1,19 @@
+// This is a generated file. Modifications will be overwritten.
+package {{ package_name }};
+
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+
+public abstract class BaseHookHandler {
+
+ public abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackT callbackContext,
+ final Logger logger,
+ final ConfigurationT typeConfiguration);
+
+}
diff --git a/python/rpdk/java/templates/generate/hook/HookHandlerWrapper.java b/python/rpdk/java/templates/generate/hook/HookHandlerWrapper.java
new file mode 100644
index 00000000..c900f218
--- /dev/null
+++ b/python/rpdk/java/templates/generate/hook/HookHandlerWrapper.java
@@ -0,0 +1,184 @@
+// This is a generated file. Modifications will be overwritten.
+package {{ package_name }};
+
+import com.amazonaws.AmazonServiceException;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.cloudformation.HookInvocationPoint;
+import software.amazon.cloudformation.exceptions.BaseHandlerException;
+import software.amazon.cloudformation.{{ wrapper_parent }};
+import software.amazon.cloudformation.loggers.LambdaLogPublisher;
+import software.amazon.cloudformation.metrics.MetricsPublisher;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.HookInvocationRequest;
+import software.amazon.cloudformation.proxy.LoggerProxy;
+import software.amazon.cloudformation.proxy.RequestContext;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookHandlerTestPayload;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import software.amazon.cloudformation.resource.Serializer;
+import software.amazon.cloudformation.scheduler.CloudWatchScheduler;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import java.io.BufferedReader;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.commons.io.IOUtils;
+import org.json.JSONObject;
+
+public final class {{ "HookHandlerWrapper" if wrapper_parent == "HookLambdaWrapper" else "HookHandlerWrapperExecutable" }} extends {{ wrapper_parent }} {
+
+ private final Configuration configuration = new Configuration();
+ private JSONObject hookSchema;
+ private final Map> handlers = new HashMap<>();
+ private final static TypeReference> REQUEST_REFERENCE =
+ new TypeReference>() {};
+ private final static TypeReference TYPE_REFERENCE =
+ new TypeReference() {};
+ private final static TypeReference> TEST_ENTRY_TYPE_REFERENCE =
+ new TypeReference>() {};
+
+ public {{ "HookHandlerWrapper" if wrapper_parent == "HookLambdaWrapper" else "HookHandlerWrapperExecutable" }}() {
+ initialiseHandlers();
+ }
+
+ private void initialiseHandlers() {
+{% for op in operations %}
+ handlers.put(HookInvocationPoint.fromShortName("{{ op }}"), new {{ op|uppercase_first_letter }}HookHandler());
+{% endfor %}
+ }
+
+ @Override
+ public ProgressEvent invokeHandler(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final HookInvocationPoint invocationPoint,
+ final CallbackContext callbackContext,
+ final TypeConfigurationModel typeConfiguration) {
+
+ final String invocationPointName = (invocationPoint == null) ? "" : invocationPoint.toString(); // paranoia
+ if (!handlers.containsKey(invocationPoint))
+ throw new RuntimeException("Unknown invocationPoint " + invocationPointName);
+
+ final BaseHookHandler handler = handlers.get(invocationPoint);
+
+ loggerProxy.log(String.format("[%s] invoking handler...", invocationPointName));
+ final ProgressEvent result = handler.handleRequest(proxy, request, callbackContext, loggerProxy, typeConfiguration);
+ loggerProxy.log(String.format("[%s] handler invoked", invocationPointName));
+ return result;
+ }
+
+ {% if wrapper_parent == "HookLambdaWrapper" -%}
+
+
+ public void testEntrypoint(
+ final InputStream inputStream,
+ final OutputStream outputStream,
+ final Context context) throws IOException {
+
+ this.loggerProxy = new LoggerProxy();
+ this.loggerProxy.addLogPublisher(new LambdaLogPublisher(context.getLogger()));
+
+ ProgressEvent response = ProgressEvent.failed(null, null, HandlerErrorCode.InternalFailure, "Uninitialized");
+ try {
+ final String input = IOUtils.toString(inputStream, "UTF-8");
+ final HookHandlerTestPayload payload =
+ this.serializer.deserialize(
+ input,
+ TEST_ENTRY_TYPE_REFERENCE);
+
+ final AmazonWebServicesClientProxy proxy = new AmazonWebServicesClientProxy(
+ loggerProxy, payload.getCredentials(), () -> (long) context.getRemainingTimeInMillis());
+
+ response = invokeHandler(proxy, payload.getRequest(), payload.getActionInvocationPoint(), payload.getCallbackContext(), payload.getTypeConfiguration());
+ } catch (final BaseHandlerException e) {
+ response = ProgressEvent.defaultFailureHandler(e, e.getErrorCode());
+ } catch (final AmazonServiceException | AwsServiceException e) {
+ response = ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.GeneralServiceException);
+ } catch (final Throwable e) {
+ e.printStackTrace();
+ response = ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.InternalFailure);
+ } finally {
+ final String output = this.serializer.serialize(response);
+ outputStream.write(output.getBytes(Charset.forName("UTF-8")));
+ outputStream.close();
+ }
+ }
+ {% else %}
+ public static void main(final String[] args) throws IOException {
+ if (args.length != 1){
+ System.exit(1);
+ }
+ final String outputFile = (UUID.randomUUID().toString() + ".txt");
+ try(final FileOutputStream output = new FileOutputStream(outputFile)){
+ try(final InputStream input = IOUtils.toInputStream(args[0],"UTF-8")){
+ new HookHandlerWrapperExecutable().handleRequest(input, output);
+ output.flush();
+ }
+ }
+ System.out.println("__CFN_HOOK_START_RESPONSE__");
+ readFileToSystemOut(outputFile);
+ System.out.println("__CFN_HOOK_END_RESPONSE__");
+ }
+
+ private static void readFileToSystemOut(final String fileName) throws IOException {
+ //Create object of FileReader
+ final FileReader inputFile = new FileReader(fileName);
+ try(final BufferedReader bufferReader = new BufferedReader(inputFile)) {
+ String line;
+ while ((line = bufferReader.readLine()) != null) {
+ System.out.println(line);
+ }
+ }
+ }
+ {%- endif %}
+
+ @Override
+ public JSONObject provideHookSchemaJSONObject() {
+ if (hookSchema == null) {
+ hookSchema = this.configuration.hookSchemaJSONObject();
+ }
+ return hookSchema;
+ }
+
+ @Override
+ protected HookHandlerRequest transform(final HookInvocationRequest request) throws IOException {
+ return HookHandlerRequest.builder()
+ .clientRequestToken(request.getClientRequestToken())
+ .hookContext(HookContext
+ .builder()
+ .awsAccountId(request.getAwsAccountId())
+ .stackId(request.getStackId())
+ .changeSetId(request.getChangeSetId())
+ .hookTypeName(request.getHookTypeName())
+ .hookTypeVersion(request.getHookTypeVersion())
+ .invocationPoint(request.getActionInvocationPoint())
+ .targetName(request.getRequestData().getTargetName())
+ .targetType(request.getRequestData().getTargetType())
+ .targetLogicalId(request.getRequestData().getTargetLogicalId())
+ .targetModel(HookTargetModel.of(request.getRequestData().getTargetModel()))
+ .build())
+ .build();
+ }
+
+ @Override
+ protected TypeReference> getTypeReference() {
+ return REQUEST_REFERENCE;
+ }
+
+ @Override
+ protected TypeReference getModelTypeReference() {
+ return TYPE_REFERENCE;
+ }
+}
diff --git a/python/rpdk/java/templates/generate/hook/HookInputModel.java b/python/rpdk/java/templates/generate/hook/HookInputModel.java
new file mode 100644
index 00000000..25b533ba
--- /dev/null
+++ b/python/rpdk/java/templates/generate/hook/HookInputModel.java
@@ -0,0 +1,35 @@
+// This is a generated file. Modifications will be overwritten.
+package {{ package_name }};
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Set;
+import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.json.JSONObject;
+
+
+@Data
+@Builder
+{% if properties.items()|length > 0 %}
+@AllArgsConstructor
+{% endif %}
+@NoArgsConstructor
+@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
+public class {{ model_name|uppercase_first_letter }} {
+ @JsonIgnore
+ public static final String TYPE_NAME = "{{ type_name }}";
+
+ {% for name, type in properties.items() %}
+ @JsonProperty("{{ name }}")
+ private {{ type|translate_type }} {{ name|lowercase_first_letter|safe_reserved }};
+ {% endfor %}
+}
diff --git a/python/rpdk/java/templates/generate/hook/ResourceHookTarget.java b/python/rpdk/java/templates/generate/hook/ResourceHookTarget.java
new file mode 100644
index 00000000..edf0b058
--- /dev/null
+++ b/python/rpdk/java/templates/generate/hook/ResourceHookTarget.java
@@ -0,0 +1,125 @@
+// This is a generated file. Modifications will be overwritten.
+package {{ package_name }};
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Set;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.json.JSONObject;
+import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTarget;
+
+
+@Data
+@Builder
+{% if properties.items()|length > 0 %}
+@AllArgsConstructor
+{% endif %}
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+@ToString
+@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
+public class {{ model_name|uppercase_first_letter }} extends ResourceHookTarget {
+ @JsonIgnore
+ public static final String TYPE_NAME = "{{ type_name }}";
+
+ {% for identifier in primaryIdentifier %}
+ {% set components = identifier.split("/") %}
+ @JsonIgnore
+ public static final String IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }} = "{{ identifier }}";
+ {% endfor -%}
+
+ {% for identifiers in additionalIdentifiers %}
+ {% for identifier in identifiers %}
+ {% set components = identifier.split("/") %}
+ public static final String IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }} = "{{ identifier }}";
+ {% endfor %}
+ {% endfor %}
+
+ {% for name, type in properties.items() %}
+ @JsonProperty("{{ name }}")
+ private {{ type|translate_type }} {{ name|lowercase_first_letter|safe_reserved }};
+
+ {% endfor %}
+ @JsonIgnore
+ public JSONObject getPrimaryIdentifier() {
+ final JSONObject identifier = new JSONObject();
+ {% for identifier in primaryIdentifier %}
+ {% set components = identifier.split("/") %}
+ if (this.get{{components[2]|uppercase_first_letter}}() != null
+ {%- for i in range(4, components|length + 1) -%}
+ {#- #} && this
+ {%- for component in components[2:i] -%} .get{{component|uppercase_first_letter}}() {%- endfor -%}
+ {#- #} != null
+ {%- endfor -%}
+ ) {
+ identifier.put(IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }}, this{% for component in components[2:] %}.get{{component|uppercase_first_letter}}(){% endfor %});
+ }
+
+ {% endfor %}
+ // only return the identifier if it can be used, i.e. if all components are present
+ return identifier.length() == {{ primaryIdentifier|length }} ? identifier : null;
+ }
+
+ @JsonIgnore
+ public List getAdditionalIdentifiers() {
+ final List identifiers = new ArrayList();
+ {% for identifiers in additionalIdentifiers %}
+ if (getIdentifier {%- for identifier in identifiers -%} _{{identifier.split("/")[-1]|uppercase_first_letter}} {%- endfor -%} () != null) {
+ identifiers.add(getIdentifier{% for identifier in identifiers %}_{{identifier.split("/")[-1]|uppercase_first_letter}}{% endfor %}());
+ }
+ {% endfor %}
+ // only return the identifiers if any can be used
+ return identifiers.isEmpty() ? null : identifiers;
+ }
+ {% for identifiers in additionalIdentifiers %}
+
+ @JsonIgnore
+ public JSONObject getIdentifier {%- for identifier in identifiers -%} _{{identifier.split("/")[-1]|uppercase_first_letter}} {%- endfor -%} () {
+ final JSONObject identifier = new JSONObject();
+ {% for identifier in identifiers %}
+ {% set components = identifier.split("/") %}
+ if (this.get{{components[2]|uppercase_first_letter}}() != null
+ {%- for i in range(4, components|length + 1) -%}
+ {#- #} && this
+ {%- for component in components[2:i] -%} .get{{component|uppercase_first_letter}}() {%- endfor -%}
+ {#- #} != null
+ {%- endfor -%}
+ ) {
+ identifier.put(IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }}, this{% for component in components[2:] %}.get{{component|uppercase_first_letter}}(){% endfor %});
+ }
+
+ {% endfor %}
+ // only return the identifier if it can be used, i.e. if all components are present
+ return identifier.length() == {{ identifiers|length }} ? identifier : null;
+ }
+ {% endfor %}
+
+ @JsonIgnore
+ public JSONObject targetSchemaJSONObject() {
+ if (targetSchema == null) {
+ targetSchema = loadSchema("{{schema_path or 'null'}}");
+ }
+
+ return targetSchema;
+ }
+
+ @JsonIgnore
+ public Boolean hasDefinedSchema() {
+ return Boolean.{{schema_available|upper}};
+ }
+
+ @JsonIgnore
+ public Boolean isCloudFormationRegistryType() {
+ return Boolean.{{is_registry_type|upper}};
+ }
+}
diff --git a/python/rpdk/java/templates/generate/hook/ResourceHookTargetModel.java b/python/rpdk/java/templates/generate/hook/ResourceHookTargetModel.java
new file mode 100644
index 00000000..dd34943f
--- /dev/null
+++ b/python/rpdk/java/templates/generate/hook/ResourceHookTargetModel.java
@@ -0,0 +1,53 @@
+// This is a generated file. Modifications will be overwritten.
+package {{ package_name }};
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Set;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.json.JSONObject;
+import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
+
+
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
+@JsonDeserialize(as = {{ model_name|uppercase_first_letter }}TargetModel.class)
+public class {{ model_name|uppercase_first_letter }}TargetModel extends ResourceHookTargetModel<{{model_name}}> {
+
+ @JsonIgnore
+ private static final TypeReference<{{model_name}}> TARGET_REFERENCE =
+ new TypeReference<{{model_name}}>() {};
+
+ @JsonIgnore
+ private static final TypeReference<{{model_name}}TargetModel> MODEL_REFERENCE =
+ new TypeReference<{{model_name}}TargetModel>() {};
+
+ @JsonIgnore
+ public static final String TARGET_TYPE_NAME = "{{ type_name }}";
+
+
+ @JsonIgnore
+ public TypeReference<{{model_name}}> getHookTargetTypeReference() {
+ return TARGET_REFERENCE;
+ }
+
+ @JsonIgnore
+ public TypeReference<{{model_name}}TargetModel> getTargetModelTypeReference() {
+ return MODEL_REFERENCE;
+ }
+}
diff --git a/python/rpdk/java/templates/init/default/StubHookHandler.java b/python/rpdk/java/templates/init/default/StubHookHandler.java
new file mode 100644
index 00000000..13f47d1c
--- /dev/null
+++ b/python/rpdk/java/templates/init/default/StubHookHandler.java
@@ -0,0 +1,44 @@
+package {{ package_name }};
+
+import com.google.common.collect.ImmutableSet;
+import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+
+import java.util.Collection;
+
+public class {{ operation }}HookHandler extends BaseHookHandler {
+
+ private static final Collection HOOK_TARGET_NAMES = ImmutableSet.of(
+ {% for target_name in target_names %}
+ "{{target_name}}"{% if not loop.last %},{% endif %}
+
+ {% endfor %}
+ );
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration) {
+
+ final String targetName = request.getHookContext().getTargetName();
+
+ if (!HOOK_TARGET_NAMES.contains(targetName)) {
+ throw new UnsupportedTargetException(targetName);
+ }
+
+ final String resultMessage = "Successfully invoked {{ operation }}HookHandler for target: " + targetName;
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .message(resultMessage)
+ .build();
+ }
+}
diff --git a/python/rpdk/java/templates/init/default/StubHookHandlerTest.java b/python/rpdk/java/templates/init/default/StubHookHandlerTest.java
new file mode 100644
index 00000000..ff262e5d
--- /dev/null
+++ b/python/rpdk/java/templates/init/default/StubHookHandlerTest.java
@@ -0,0 +1,53 @@
+package {{ package_name }};
+
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+@ExtendWith(MockitoExtension.class)
+public class {{ operation }}HookHandlerTest {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private Logger logger;
+
+ @BeforeEach
+ public void setup() {
+ proxy = mock(AmazonWebServicesClientProxy.class);
+ logger = mock(Logger.class);
+ }
+
+ @Test
+ public void handleRequest_SimpleSuccess() {
+ final {{ operation }}HookHandler handler = new {{ operation }}HookHandler();
+
+ final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().build();
+
+ final HookHandlerRequest request = HookHandlerRequest.builder()
+ .hookContext(HookContext.builder().targetName("{{target_names|first}}").build())
+ .build();
+
+ final ProgressEvent response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackContext()).isNull();
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNotNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/BaseHookHandlerStd.java b/python/rpdk/java/templates/init/guided_aws/BaseHookHandlerStd.java
new file mode 100644
index 00000000..ec41f383
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/BaseHookHandlerStd.java
@@ -0,0 +1,40 @@
+package {{ package_name }};
+
+import software.amazon.awssdk.core.SdkClient;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+
+// Placeholder for the functionality that could be shared across PreCreate/PreUpdate/PreDelete Handlers
+
+public abstract class BaseHookHandlerStd extends BaseHookHandler {
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration
+ ) {
+ return handleRequest(
+ proxy,
+ request,
+ callbackContext != null ? callbackContext : new CallbackContext(),
+ proxy.newProxy(ClientBuilder::getClient),
+ logger,
+ typeConfiguration
+ );
+ }
+
+ protected abstract ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration
+ );
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/HookTranslator.java b/python/rpdk/java/templates/init/guided_aws/HookTranslator.java
new file mode 100644
index 00000000..1ffa418e
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/HookTranslator.java
@@ -0,0 +1,48 @@
+package {{ package_name }};
+
+
+import {{ package_name }}.model.my.example.resource.MyExampleResourceTargetModel;
+import {{ package_name }}.model.other.example.resource.OtherExampleResourceTargetModel;
+import software.amazon.awssdk.awscore.AwsRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+
+/**
+ * This class is a centralized placeholder for
+ * - api request construction
+ * - object translation to/from aws sdk
+ */
+
+public class Translator {
+
+ /**
+ * Request built from target model
+ * @param targetModel target model
+ * @return awsRequest the aws service request
+ */
+ static AwsRequest translateToRequest(final HookTargetModel targetModel) {
+ final AwsRequest awsRequest;
+ if (targetModel instanceof MyExampleResourceTargetModel) {
+ awsRequest = translateToRequest((MyExampleResourceTargetModel) targetModel);
+ } else if (targetModel instanceof OtherExampleResourceTargetModel) {
+ awsRequest = translateToRequest((OtherExampleResourceTargetModel) targetModel);
+ } else {
+ // TODO: construct a request
+ awsRequest = null;
+ }
+
+ return awsRequest;
+ }
+
+ static AwsRequest translateToRequest(final MyExampleResourceTargetModel targetModel) {
+ final AwsRequest awsRequest = null;
+ // TODO: construct a request
+ return awsRequest;
+ }
+
+ static AwsRequest translateToRequest(final OtherExampleResourceTargetModel targetModel) {
+ final AwsRequest awsRequest = null;
+ // TODO: construct a request
+ return awsRequest;
+ }
+
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreCreateHookHandler.java b/python/rpdk/java/templates/init/guided_aws/StubPreCreateHookHandler.java
new file mode 100644
index 00000000..7200c0b2
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreCreateHookHandler.java
@@ -0,0 +1,74 @@
+package {{ package_name }};
+
+import {{ package_name }}.model.my.example.resource.MyExampleResource;
+import {{ package_name }}.model.my.example.resource.MyExampleResourceTargetModel;
+import software.amazon.awssdk.core.SdkClient;
+import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
+
+public class {{ operation }}HookHandler extends BaseHookHandlerStd {
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration) {
+
+ final HookContext hookContext = request.getHookContext();
+ final String targetName = hookContext.getTargetName();
+
+ if (!"My::Example::Resource".equals(targetName)) {
+ throw new UnsupportedTargetException(targetName);
+ }
+
+ logger.log(String.format("Successfully invoked {{ operation }}HookHandler for target %s.", targetName));
+
+ final String expectedEncryptionAlgorithm = typeConfiguration.getEncryptionAlgorithm();
+ logger.log(String.format("Verifying server side encryption for target %s, expecting target server side encryption algorithm to be %s.",
+ targetName, expectedEncryptionAlgorithm));
+
+ final ResourceHookTargetModel targetModel = hookContext.getTargetModel(MyExampleResourceTargetModel.class);
+
+ final MyExampleResource resourceProperties = targetModel.getResourceProperties();
+ final String targetEncryptionAlgorithm = (String) resourceProperties.get("MyEncryptionAlgorithm");
+
+ if (targetEncryptionAlgorithm == null) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, target does not have server side encryption enabled.",
+ targetName);
+ logger.log(failureMessage);
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ if (!targetEncryptionAlgorithm.equals(expectedEncryptionAlgorithm)) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, expecting encryption algorithm to be %s, acutal encryption algorithm is %s",
+ targetName, expectedEncryptionAlgorithm, targetEncryptionAlgorithm);
+ logger.log(failureMessage);
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ final String successMessage = String.format("Successfully verified server side encryption for target %s.", targetName);
+ return ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .message(successMessage)
+ .build();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreCreateHookHandlerTest.java b/python/rpdk/java/templates/init/guided_aws/StubPreCreateHookHandlerTest.java
new file mode 100644
index 00000000..72f9f2ec
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreCreateHookHandlerTest.java
@@ -0,0 +1,62 @@
+package {{ package_name }};
+
+import java.util.Map;
+import java.util.HashMap;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class {{ operation }}HookHandlerTest extends AbstractTestBase {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private Logger logger;
+
+ @BeforeEach
+ public void setup() {
+ proxy = mock(AmazonWebServicesClientProxy.class);
+ logger = mock(Logger.class);
+ }
+
+ @Test
+ public void handleRequest_SimpleSuccess() {
+ final {{ operation }}HookHandler handler = new {{ operation }}HookHandler();
+
+ final TypeConfigurationModel typeConfiguration = mock(TypeConfigurationModel.class);
+ when(typeConfiguration.getEncryptionAlgorithm()).thenReturn("AES256");
+
+ final Map targetModel = new HashMap<>();
+ final Map resourceProperties = new HashMap<>();
+ resourceProperties.put("MyEncryptionAlgorithm", "AES256");
+ targetModel.put("ResourceProperties", resourceProperties);
+
+ final HookHandlerRequest request = HookHandlerRequest.builder()
+ .hookContext(HookContext.builder().targetName("My::Example::Resource").targetModel(HookTargetModel.of(targetModel)).build())
+ .build();
+
+ final ProgressEvent response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackContext()).isNull();
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNotNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreDeleteHookHandler.java b/python/rpdk/java/templates/init/guided_aws/StubPreDeleteHookHandler.java
new file mode 100644
index 00000000..6b41f772
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreDeleteHookHandler.java
@@ -0,0 +1,74 @@
+package {{ package_name }};
+
+import {{ package_name }}.model.other.example.resource.OtherExampleResource;
+import {{ package_name }}.model.other.example.resource.OtherExampleResourceTargetModel;
+import software.amazon.awssdk.core.SdkClient;
+import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
+
+public class {{ operation }}HookHandler extends BaseHookHandlerStd {
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration) {
+
+ final HookContext hookContext = request.getHookContext();
+ final String targetName = hookContext.getTargetName();
+
+ if (!"Other::Example::Resource".equals(targetName)) {
+ throw new UnsupportedTargetException(targetName);
+ }
+
+ logger.log(String.format("Successfully invoked {{ operation }}HookHandler for target %s.", targetName));
+
+ final String expectedEncryptionAlgorithm = typeConfiguration.getEncryptionAlgorithm();
+ logger.log(String.format("Verifying server side encryption for target %s, expecting target server side encryption algorithm to be %s.",
+ targetName, expectedEncryptionAlgorithm));
+
+ final ResourceHookTargetModel targetModel = hookContext.getTargetModel(OtherExampleResourceTargetModel.class);
+
+ final OtherExampleResource resourceProperties = targetModel.getResourceProperties();
+ final String targetEncryptionAlgorithm = (String) resourceProperties.get("OtherEncryptionAlgorithm");
+
+ if (targetEncryptionAlgorithm == null) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, target does not have server side encryption enabled.",
+ targetName);
+ logger.log(failureMessage);
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ if (!targetEncryptionAlgorithm.equals(expectedEncryptionAlgorithm)) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, expecting encryption algorithm to be %s, acutal encryption algorithm is %s",
+ targetName, expectedEncryptionAlgorithm, targetEncryptionAlgorithm);
+ logger.log(failureMessage);
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ final String successMessage = String.format("Successfully verified server side encryption for target %s.", targetName);
+ return ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .message(successMessage)
+ .build();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreDeleteHookHandlerTest.java b/python/rpdk/java/templates/init/guided_aws/StubPreDeleteHookHandlerTest.java
new file mode 100644
index 00000000..5bb2e516
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreDeleteHookHandlerTest.java
@@ -0,0 +1,62 @@
+package {{ package_name }};
+
+import java.util.Map;
+import java.util.HashMap;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class {{ operation }}HookHandlerTest extends AbstractTestBase {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private Logger logger;
+
+ @BeforeEach
+ public void setup() {
+ proxy = mock(AmazonWebServicesClientProxy.class);
+ logger = mock(Logger.class);
+ }
+
+ @Test
+ public void handleRequest_SimpleSuccess() {
+ final {{ operation }}HookHandler handler = new {{ operation }}HookHandler();
+
+ final TypeConfigurationModel typeConfiguration = mock(TypeConfigurationModel.class);
+ when(typeConfiguration.getEncryptionAlgorithm()).thenReturn("AES256");
+
+ final Map targetModel = new HashMap<>();
+ final Map resourceProperties = new HashMap<>();
+ resourceProperties.put("OtherEncryptionAlgorithm", "AES256");
+ targetModel.put("ResourceProperties", resourceProperties);
+
+ final HookHandlerRequest request = HookHandlerRequest.builder()
+ .hookContext(HookContext.builder().targetName("Other::Example::Resource").targetModel(HookTargetModel.of(targetModel)).build())
+ .build();
+
+ final ProgressEvent response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackContext()).isNull();
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNotNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreImportHookHandler.java b/python/rpdk/java/templates/init/guided_aws/StubPreImportHookHandler.java
new file mode 100644
index 00000000..f8748843
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreImportHookHandler.java
@@ -0,0 +1,68 @@
+package {{ package_name }};
+
+import {{ package_name }}.model.my.example.resource.MyExampleResourceTargetModel;
+import {{ package_name }}.model.other.example.resource.OtherExampleResourceTargetModel;
+import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.HookStatus;
+import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+
+public class {{ operation }}HookHandler extends BaseHookHandler {
+
+ @Override
+ public HookProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration) {
+
+ final HookContext hookContext = request.getHookContext();
+ final String targetName = hookContext.getTargetName();
+ logger.log(String.format("Successfully invoked {{ operation }}HookHandler for target %s.", targetName));
+
+ final String expectedEncryptionAlgorithm = typeConfiguration.getEncryptionAlgorithm();
+ logger.log(String.format("Verifying server side encryption for target %s, expecting target server side encryption algorithm to be %s.",
+ targetName, expectedEncryptionAlgorithm));
+
+ final String targetEncryptionAlgorithm;
+ if ("My::Example::Resource".equals(targetName)) {
+ targetEncryptionAlgorithm = (String) hookContext.getTargetModel(MyExampleResourceTargetModel.class).getResourceProperties().get("MyEncryptionAlgorithm");
+ } else if ("Other::Example::Resource".equals(targetName)) {
+ targetEncryptionAlgorithm = (String) hookContext.getTargetModel(OtherExampleResourceTargetModel.class).getResourceProperties().get("OtherEncryptionAlgorithm");
+ } else {
+ throw new UnsupportedTargetException(targetName);
+ }
+
+ if (targetEncryptionAlgorithm == null) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, target does not have server side encryption enabled.",
+ targetName);
+ logger.log(failureMessage);
+
+ return HookProgressEvent.builder()
+ .status(HookStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ if (!targetEncryptionAlgorithm.equals(expectedEncryptionAlgorithm)) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, expecting encryption algorithm to be %s, acutal encryption algorithm is %s",
+ targetName, expectedEncryptionAlgorithm, targetEncryptionAlgorithm);
+ logger.log(failureMessage);
+
+ return HookProgressEvent.builder()
+ .status(HookStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ final String successMessage = String.format("Successfully verified server side encryption for target %s.", targetName);
+ return HookProgressEvent.builder()
+ .status(HookStatus.SUCCESS)
+ .message(successMessage)
+ .build();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreImportHookHandlerTest.java b/python/rpdk/java/templates/init/guided_aws/StubPreImportHookHandlerTest.java
new file mode 100644
index 00000000..3240d99a
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreImportHookHandlerTest.java
@@ -0,0 +1,88 @@
+package {{ package_name }};
+
+import java.util.Map;
+import java.util.HashMap;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.hook.HookStatus;
+import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class {{ operation }}HookHandlerTest {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private Logger logger;
+
+ @BeforeEach
+ public void setup() {
+ proxy = mock(AmazonWebServicesClientProxy.class);
+ logger = mock(Logger.class);
+ }
+
+ @Test
+ public void handleRequest_SimpleSuccess_OtherExampleResource() {
+ final {{ operation }}HookHandler handler = new {{ operation }}HookHandler();
+
+ final TypeConfigurationModel typeConfiguration = mock(TypeConfigurationModel.class);
+ when(typeConfiguration.getEncryptionAlgorithm()).thenReturn("AES256");
+
+ final Map targetModel = new HashMap<>();
+ final Map resourceProperties = new HashMap<>();
+ resourceProperties.put("OtherEncryptionAlgorithm", "AES256");
+ targetModel.put("ResourceProperties", resourceProperties);
+
+ final HookHandlerRequest request = HookHandlerRequest.builder()
+ .hookContext(HookContext.builder().targetName("Other::Example::Resource").targetModel(HookTargetModel.of(targetModel)).build())
+ .build();
+
+ final HookProgressEvent response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(HookStatus.SUCCESS);
+ assertThat(response.getCallbackContext()).isNull();
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNotNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+
+ @Test
+ public void handleRequest_SimpleSuccess_MyExampleResource() {
+ final {{ operation }}HookHandler handler = new {{ operation }}HookHandler();
+
+ final TypeConfigurationModel typeConfiguration = mock(TypeConfigurationModel.class);
+ when(typeConfiguration.getEncryptionAlgorithm()).thenReturn("AES256");
+
+ final Map targetModel = new HashMap<>();
+ final Map resourceProperties = new HashMap<>();
+ resourceProperties.put("MyEncryptionAlgorithm", "AES256");
+ targetModel.put("ResourceProperties", resourceProperties);
+
+ final HookHandlerRequest request = HookHandlerRequest.builder()
+ .hookContext(HookContext.builder().targetName("My::Example::Resource").targetModel(HookTargetModel.of(targetModel)).build())
+ .build();
+
+ final HookProgressEvent response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(HookStatus.SUCCESS);
+ assertThat(response.getCallbackContext()).isNull();
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNotNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreUpdateHookHandler.java b/python/rpdk/java/templates/init/guided_aws/StubPreUpdateHookHandler.java
new file mode 100644
index 00000000..7a2b15b8
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreUpdateHookHandler.java
@@ -0,0 +1,78 @@
+package {{ package_name }};
+
+import {{ package_name }}.model.my.example.resource.MyExampleResource;
+import {{ package_name }}.model.my.example.resource.MyExampleResourceTargetModel;
+import software.amazon.awssdk.core.SdkClient;
+import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.ProxyClient;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
+
+public class {{ operation }}HookHandler extends BaseHookHandlerStd {
+
+ @Override
+ public ProgressEvent handleRequest(
+ final AmazonWebServicesClientProxy proxy,
+ final HookHandlerRequest request,
+ final CallbackContext callbackContext,
+ final ProxyClient proxyClient,
+ final Logger logger,
+ final TypeConfigurationModel typeConfiguration) {
+
+ final HookContext hookContext = request.getHookContext();
+ final String targetName = hookContext.getTargetName();
+
+ if (!"My::Example::Resource".equals(targetName)) {
+ throw new UnsupportedTargetException(targetName);
+ }
+
+ logger.log(String.format("Successfully invoked {{ operation }}HookHandler for target %s.", targetName));
+
+ final String expectedEncryptionAlgorithm = typeConfiguration.getEncryptionAlgorithm();
+ logger.log(String.format("Verifying server side encryption for target %s, expecting target server side encryption algorithm to be %s.",
+ targetName, expectedEncryptionAlgorithm));
+
+ final ResourceHookTargetModel targetModel = hookContext.getTargetModel(MyExampleResourceTargetModel.class);
+
+ final MyExampleResource previousResourceProperties = targetModel.getPreviousResourceProperties();
+ final String previousTargetEncryptionAlgorithm = (String) previousResourceProperties.get("MyEncryptionAlgorithm");
+ logger.log(String.format("Target resource's previous server side encryption algorithm is %s.", previousTargetEncryptionAlgorithm));
+
+ final MyExampleResource resourceProperties = targetModel.getResourceProperties();
+ final String targetEncryptionAlgorithm = (String) resourceProperties.get("MyEncryptionAlgorithm");
+
+ if (targetEncryptionAlgorithm == null) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, target does not have server side encryption enabled.",
+ targetName);
+ logger.log(failureMessage);
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ if (!targetEncryptionAlgorithm.equals(expectedEncryptionAlgorithm)) {
+ final String failureMessage = String.format("Failed to verify server side encryption for target %s, expecting encryption algorithm to be %s, acutal encryption algorithm is %s",
+ targetName, expectedEncryptionAlgorithm, targetEncryptionAlgorithm);
+ logger.log(failureMessage);
+
+ return ProgressEvent.builder()
+ .status(OperationStatus.FAILED)
+ .message(failureMessage)
+ .build();
+ }
+
+ final String successMessage = String.format("Successfully verified server side encryption for target %s.", targetName);
+ return ProgressEvent.builder()
+ .status(OperationStatus.SUCCESS)
+ .message(successMessage)
+ .build();
+ }
+}
diff --git a/python/rpdk/java/templates/init/guided_aws/StubPreUpdateHookHandlerTest.java b/python/rpdk/java/templates/init/guided_aws/StubPreUpdateHookHandlerTest.java
new file mode 100644
index 00000000..e8968146
--- /dev/null
+++ b/python/rpdk/java/templates/init/guided_aws/StubPreUpdateHookHandlerTest.java
@@ -0,0 +1,63 @@
+package {{ package_name }};
+
+import java.util.Map;
+import java.util.HashMap;
+import software.amazon.cloudformation.proxy.Logger;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookContext;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class {{ operation }}HookHandlerTest extends AbstractTestBase {
+
+ @Mock
+ private AmazonWebServicesClientProxy proxy;
+
+ @Mock
+ private Logger logger;
+
+ @BeforeEach
+ public void setup() {
+ proxy = mock(AmazonWebServicesClientProxy.class);
+ logger = mock(Logger.class);
+ }
+
+ @Test
+ public void handleRequest_SimpleSuccess() {
+ final {{ operation }}HookHandler handler = new {{ operation }}HookHandler();
+
+ final TypeConfigurationModel typeConfiguration = mock(TypeConfigurationModel.class);
+ when(typeConfiguration.getEncryptionAlgorithm()).thenReturn("AES256");
+
+ final Map targetModel = new HashMap<>();
+ final Map resourceProperties = new HashMap<>();
+ resourceProperties.put("MyEncryptionAlgorithm", "AES256");
+ targetModel.put("ResourceProperties", resourceProperties);
+ targetModel.put("PreviousResourceProperties", new HashMap<>());
+
+ final HookHandlerRequest request = HookHandlerRequest.builder()
+ .hookContext(HookContext.builder().targetName("My::Example::Resource").targetModel(HookTargetModel.of(targetModel)).build())
+ .build();
+
+ final ProgressEvent response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
+
+ assertThat(response).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
+ assertThat(response.getCallbackContext()).isNull();
+ assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
+ assertThat(response.getMessage()).isNotNull();
+ assertThat(response.getErrorCode()).isNull();
+ }
+}
diff --git a/python/rpdk/java/templates/init/shared/README_HOOK.md b/python/rpdk/java/templates/init/shared/README_HOOK.md
new file mode 100644
index 00000000..1711daa8
--- /dev/null
+++ b/python/rpdk/java/templates/init/shared/README_HOOK.md
@@ -0,0 +1,12 @@
+# {{ type_name }}
+
+Congratulations on starting development! Next steps:
+
+1. Write the JSON schema describing your hook, `{{ schema_path.name }}`
+1. Implement your hook handlers.
+
+The RPDK will automatically generate the correct hook input model from the schema whenever the project is built via Maven. You can also do this manually with the following command: `{{ executable }} generate`.
+
+> Please don't modify files under `target/generated-sources/rpdk`, as they will be automatically overwritten.
+
+The code uses [Lombok](https://projectlombok.org/), and [you may have to install IDE integrations](https://projectlombok.org/) to enable auto-complete for Lombok-annotated classes.
diff --git a/python/rpdk/java/templates/init/shared/README.md b/python/rpdk/java/templates/init/shared/README_RESOURCE.md
similarity index 100%
rename from python/rpdk/java/templates/init/shared/README.md
rename to python/rpdk/java/templates/init/shared/README_RESOURCE.md
diff --git a/python/rpdk/java/templates/init/shared/StubHookConfiguration.java b/python/rpdk/java/templates/init/shared/StubHookConfiguration.java
new file mode 100644
index 00000000..40f7a6f7
--- /dev/null
+++ b/python/rpdk/java/templates/init/shared/StubHookConfiguration.java
@@ -0,0 +1,8 @@
+package {{ package_name }};
+
+class Configuration extends BaseHookConfiguration {
+
+ public Configuration() {
+ super("{{ schema_file_name }}");
+ }
+}
diff --git a/python/rpdk/java/templates/init/shared/pom.xml b/python/rpdk/java/templates/init/shared/pom.xml
index cdf39ffc..45bc4b9e 100644
--- a/python/rpdk/java/templates/init/shared/pom.xml
+++ b/python/rpdk/java/templates/init/shared/pom.xml
@@ -16,6 +16,7 @@
1.8
UTF-8
UTF-8
+
@@ -100,6 +101,14 @@
2.3
false
+
+
+ *:*
+
+ **/Log4j2Plugins.dat
+
+
+
@@ -123,7 +132,7 @@
{{ executable }}
- generate
+ generate ${cfn.generate.args}
${project.basedir}
@@ -217,6 +226,12 @@
{{ schema_file_name }}
+
+ ${project.basedir}/target/loaded-target-schemas
+
+ **/*.json
+
+
diff --git a/setup.py b/setup.py
index ed71b29a..e84ad6ac 100644
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ def find_version(*file_paths):
# package_data -> use MANIFEST.in instead
include_package_data=True,
zip_safe=True,
- install_requires=["cloudformation-cli>=0.2.13"],
+ install_requires=["cloudformation-cli>=0.2.23"],
python_requires=">=3.6",
entry_points={"rpdk.v1.languages": ["java = rpdk.java.codegen:JavaLanguagePlugin"]},
license="Apache License 2.0",
diff --git a/src/main/java/software/amazon/cloudformation/AbstractWrapper.java b/src/main/java/software/amazon/cloudformation/AbstractWrapper.java
index 10631d68..8bedfad1 100644
--- a/src/main/java/software/amazon/cloudformation/AbstractWrapper.java
+++ b/src/main/java/software/amazon/cloudformation/AbstractWrapper.java
@@ -409,6 +409,11 @@ protected void writeResponse(final OutputStream outputStream, final ProgressEven
}
}
+ if (response.getResult() != null) {
+ // remove any non-resource specific fields from response
+ response.setResult(null);
+ }
+
String output = this.serializer.serialize(response);
outputStream.write(output.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java
new file mode 100644
index 00000000..64892918
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java
@@ -0,0 +1,523 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation;
+
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.retry.RetryUtils;
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.http.HttpStatusCode;
+import software.amazon.awssdk.http.HttpStatusFamily;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.utils.StringUtils;
+import software.amazon.cloudformation.encryption.Cipher;
+import software.amazon.cloudformation.encryption.KMSCipher;
+import software.amazon.cloudformation.exceptions.BaseHandlerException;
+import software.amazon.cloudformation.exceptions.FileScrubberException;
+import software.amazon.cloudformation.exceptions.TerminalException;
+import software.amazon.cloudformation.injection.CloudWatchLogsProvider;
+import software.amazon.cloudformation.injection.CloudWatchProvider;
+import software.amazon.cloudformation.injection.CredentialsProvider;
+import software.amazon.cloudformation.injection.SessionCredentialsProvider;
+import software.amazon.cloudformation.loggers.CloudWatchLogHelper;
+import software.amazon.cloudformation.loggers.CloudWatchLogPublisher;
+import software.amazon.cloudformation.loggers.LogPublisher;
+import software.amazon.cloudformation.metrics.HookMetricsPublisherImpl;
+import software.amazon.cloudformation.metrics.MetricsPublisher;
+import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
+import software.amazon.cloudformation.proxy.Credentials;
+import software.amazon.cloudformation.proxy.DelayFactory;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.LoggerProxy;
+import software.amazon.cloudformation.proxy.MetricsPublisherProxy;
+import software.amazon.cloudformation.proxy.OperationStatus;
+import software.amazon.cloudformation.proxy.ProgressEvent;
+import software.amazon.cloudformation.proxy.WaitStrategy;
+import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
+import software.amazon.cloudformation.proxy.hook.HookInvocationRequest;
+import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
+import software.amazon.cloudformation.proxy.hook.HookRequestContext;
+import software.amazon.cloudformation.proxy.hook.HookStatus;
+import software.amazon.cloudformation.resource.SchemaValidator;
+import software.amazon.cloudformation.resource.Serializer;
+import software.amazon.cloudformation.resource.Validator;
+import software.amazon.cloudformation.resource.exceptions.ValidationException;
+
+public abstract class HookAbstractWrapper {
+
+ public static final SdkHttpClient HTTP_CLIENT = ApacheHttpClient.builder().build();
+ private static final Logger LOG = LoggerFactory.getLogger(HookAbstractWrapper.class);
+
+ protected final Serializer serializer;
+ protected LoggerProxy loggerProxy;
+ protected MetricsPublisherProxy metricsPublisherProxy;
+ protected Cipher cipher;
+
+ protected LoggerProxy platformLoggerProxy;
+ protected LogPublisher platformLogPublisher;
+
+ // provider... prefix indicates credential provided by hook owner
+
+ final CredentialsProvider providerCredentialsProvider;
+ final CloudWatchProvider providerCloudWatchProvider;
+ final CloudWatchLogsProvider cloudWatchLogsProvider;
+ final SchemaValidator validator;
+ final TypeReference> typeReference;
+
+ private MetricsPublisher platformMetricsPublisher;
+ private MetricsPublisher providerMetricsPublisher;
+
+ private CloudWatchLogHelper cloudWatchLogHelper;
+ private CloudWatchLogPublisher providerEventsLogger;
+
+ protected HookAbstractWrapper() {
+ this.providerCredentialsProvider = new SessionCredentialsProvider();
+ this.providerCloudWatchProvider = new CloudWatchProvider(this.providerCredentialsProvider, HTTP_CLIENT);
+ this.cloudWatchLogsProvider = new CloudWatchLogsProvider(this.providerCredentialsProvider, HTTP_CLIENT);
+ this.serializer = new Serializer();
+ this.validator = new Validator();
+ this.typeReference = getTypeReference();
+ this.platformLoggerProxy = new LoggerProxy();
+ }
+
+ /*
+ * This .ctor provided for testing
+ */
+ public HookAbstractWrapper(final CredentialsProvider providerCredentialsProvider,
+ final CloudWatchLogPublisher providerEventsLogger,
+ final LogPublisher platformEventsLogger,
+ final MetricsPublisher platformMetricsPublisher,
+ final MetricsPublisher providerMetricsPublisher,
+ final SchemaValidator validator,
+ final Serializer serializer,
+ final SdkHttpClient httpClient,
+ final Cipher cipher) {
+ this.providerCredentialsProvider = providerCredentialsProvider;
+ this.providerCloudWatchProvider = new CloudWatchProvider(this.providerCredentialsProvider, httpClient);
+ this.cloudWatchLogsProvider = new CloudWatchLogsProvider(this.providerCredentialsProvider, httpClient);
+ this.providerEventsLogger = providerEventsLogger;
+ this.platformLogPublisher = platformEventsLogger;
+ this.platformMetricsPublisher = platformMetricsPublisher;
+ this.providerMetricsPublisher = providerMetricsPublisher;
+ this.serializer = serializer;
+ this.validator = validator;
+ this.typeReference = getTypeReference();
+ this.cipher = cipher;
+ this.platformLoggerProxy = new LoggerProxy();
+ }
+
+ /**
+ * This function initialises dependencies which are depending on credentials
+ * passed at function invoke and not available during construction
+ */
+ private void initialiseRuntime(final String hookTypeName,
+ final String providerCredentials,
+ final String providerLogGroupName,
+ final String awsAccountId,
+ final String hookEncryptionKeyArn,
+ final String hookEncryptionKeyRole) {
+
+ this.metricsPublisherProxy = new MetricsPublisherProxy();
+ this.loggerProxy = new LoggerProxy();
+ this.loggerProxy.addLogPublisher(this.platformLogPublisher);
+
+ // Initialize a KMS cipher to decrypt customer credentials in HookRequestData
+ if (this.cipher == null && hookEncryptionKeyArn != null && hookEncryptionKeyRole != null) {
+ this.cipher = new KMSCipher(hookEncryptionKeyArn, hookEncryptionKeyRole);
+ }
+
+ // Initialisation skipped if dependencies were set during injection (in unit
+ // tests).
+ // e.g. "if (this.platformMetricsPublisher == null)"
+ if (this.platformMetricsPublisher == null) {
+ // platformMetricsPublisher needs aws account id to differentiate metrics
+ // namespace
+ this.platformMetricsPublisher = new HookMetricsPublisherImpl(this.platformLoggerProxy, awsAccountId, hookTypeName);
+ }
+ this.metricsPublisherProxy.addMetricsPublisher(this.platformMetricsPublisher);
+ this.platformMetricsPublisher.refreshClient();
+
+ // NOTE: providerCredentials and providerLogGroupName are null/not null in
+ // sync.
+ // Both are required parameters when LoggingConfig (optional) is provided when
+ // 'RegisterType'.
+ final Credentials processedProviderCredentials = processCredentials(providerCredentials);
+ if (processedProviderCredentials != null) {
+ if (this.providerCredentialsProvider != null) {
+ this.providerCredentialsProvider.setCredentials(processedProviderCredentials);
+ }
+
+ if (this.providerMetricsPublisher == null) {
+ this.providerMetricsPublisher = new HookMetricsPublisherImpl(this.providerCloudWatchProvider, this.loggerProxy,
+ awsAccountId, hookTypeName);
+ }
+ this.metricsPublisherProxy.addMetricsPublisher(this.providerMetricsPublisher);
+ this.providerMetricsPublisher.refreshClient();
+
+ if (this.providerEventsLogger == null) {
+ this.cloudWatchLogHelper = new CloudWatchLogHelper(this.cloudWatchLogsProvider, providerLogGroupName,
+ this.platformLoggerProxy, this.metricsPublisherProxy);
+ this.cloudWatchLogHelper.refreshClient();
+
+ this.providerEventsLogger = new CloudWatchLogPublisher(this.cloudWatchLogsProvider, providerLogGroupName,
+ this.cloudWatchLogHelper.prepareLogStream(),
+ this.platformLoggerProxy, this.metricsPublisherProxy);
+ }
+ this.loggerProxy.addLogPublisher(this.providerEventsLogger);
+ this.providerEventsLogger.refreshClient();
+ }
+ }
+
+ public void processRequest(final InputStream inputStream, final OutputStream outputStream) throws IOException,
+ TerminalException {
+
+ ProgressEvent handlerResponse = null;
+ HookInvocationRequest request = null;
+ scrubFiles();
+ try {
+ if (inputStream == null) {
+ throw new TerminalException("No request object received");
+ }
+
+ String input = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+ JSONObject rawInput = new JSONObject(new JSONTokener(input));
+ // deserialize incoming payload to modeled request
+ request = this.serializer.deserialize(input, typeReference);
+ handlerResponse = processInvocation(rawInput, request);
+ } catch (final ValidationException e) {
+ String message;
+ String fullExceptionMessage = ValidationException.buildFullExceptionMessage(e);
+ if (!StringUtils.isEmpty(fullExceptionMessage)) {
+ message = String.format("Model validation failed (%s)", fullExceptionMessage);
+ } else {
+ message = "Model validation failed with unknown cause.";
+ }
+
+ handlerResponse = ProgressEvent.defaultFailureHandler(new TerminalException(message, e),
+ HandlerErrorCode.InvalidRequest);
+ publishExceptionMetric(request != null ? request.getActionInvocationPoint() : null, e,
+ HandlerErrorCode.InvalidRequest);
+ } catch (final Throwable e) {
+ // Exceptions are wrapped as a consistent error response to the caller (i.e;
+ // CloudFormation)
+ logError(ExceptionUtils.getStackTrace(e));
+
+ handlerResponse = ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.InternalFailure);
+ publishExceptionMetric(request != null ? request.getActionInvocationPoint() : null, e,
+ HandlerErrorCode.InternalFailure);
+ } finally {
+ // A response will be output on all paths, though CloudFormation will
+ // not block on invoking the handlers, but rather listen for callbacks
+ writeResponse(outputStream, createProgressResponse(handlerResponse, request));
+ publishExceptionCodeAndCountMetrics(request == null ? null : request.getActionInvocationPoint(),
+ handlerResponse.getErrorCode());
+ }
+ }
+
+ private ProgressEvent processInvocation(final JSONObject rawRequest,
+ final HookInvocationRequest request)
+ throws IOException,
+ TerminalException {
+
+ assert request != null : "Invalid request object received. Request object is null";
+
+ if (request.getRequestData() == null || request.getRequestData().getTargetModel() == null) {
+ throw new TerminalException("Invalid request object received. Target Model can not be null.");
+ }
+
+ if (request.getActionInvocationPoint() == HookInvocationPoint.UPDATE_POST_PROVISION) {
+ if (request.getRequestData().getTargetModel().get("PreviousResourceProperties") == null) {
+ throw new TerminalException("Invalid hook invocation request object received");
+ }
+ }
+
+ // TODO: Include hook schema validation here after schema is finalized
+
+ // initialise dependencies with platform credentials
+ initialiseRuntime(request.getHookTypeName(), request.getRequestData().getProviderCredentials(),
+ request.getRequestData().getProviderLogGroupName(), request.getAwsAccountId(),
+ request.getRequestData().getHookEncryptionKeyArn(), request.getRequestData().getHookEncryptionKeyRole());
+
+ // transform the request object to pass to caller
+ HookHandlerRequest hookHandlerRequest = transform(request);
+ ConfigurationT typeConfiguration = request.getHookModel();
+
+ HookRequestContext requestContext = request.getRequestContext();
+
+ this.metricsPublisherProxy.publishInvocationMetric(Instant.now(), request.getActionInvocationPoint());
+
+ // last mile proxy creation with passed-in credentials (unless we are operating
+ // in a non-AWS model)
+ AmazonWebServicesClientProxy awsClientProxy = null;
+ Credentials processedCallerCredentials = processCredentials(request.getRequestData().getCallerCredentials());
+ if (processedCallerCredentials != null) {
+ awsClientProxy = new AmazonWebServicesClientProxy(this.loggerProxy, processedCallerCredentials,
+ DelayFactory.CONSTANT_DEFAULT_DELAY_FACTORY,
+ WaitStrategy.scheduleForCallbackStrategy());
+
+ }
+
+ CallbackT callbackContext = (requestContext != null) ? requestContext.getCallbackContext() : null;
+
+ return wrapInvocationAndHandleErrors(awsClientProxy, hookHandlerRequest, request, callbackContext, typeConfiguration);
+ }
+
+ private void logUnhandledError(final String errorDescription,
+ final HookInvocationRequest request,
+ final Throwable e) {
+ logError(String.format("%s in a %s action on a %s: %s%n%s", errorDescription, request.getActionInvocationPoint(),
+ request.getHookTypeName(), e.toString(), ExceptionUtils.getStackTrace(e)));
+ }
+
+ /**
+ * Invokes the handler implementation for the request, and wraps with try-catch
+ * to consistently handle certain classes of errors and correctly map those to
+ * the appropriate HandlerErrorCode Also wraps the invocation in last-mile
+ * timing metrics
+ */
+ private ProgressEvent
+ wrapInvocationAndHandleErrors(final AmazonWebServicesClientProxy awsClientProxy,
+ final HookHandlerRequest hookHandlerRequest,
+ final HookInvocationRequest request,
+ final CallbackT callbackContext,
+ final ConfigurationT typeConfiguration) {
+
+ Date startTime = Date.from(Instant.now());
+ try {
+ ProgressEvent handlerResponse = invokeHandler(awsClientProxy, hookHandlerRequest,
+ request.getActionInvocationPoint(), callbackContext, typeConfiguration);
+ if (handlerResponse != null) {
+ this.log(String.format("Handler returned %s", handlerResponse.getStatus()));
+ } else {
+ this.logError("Handler returned null");
+ throw new TerminalException("Handler failed to provide a response.");
+ }
+
+ return handlerResponse;
+ } catch (final BaseHandlerException e) {
+ publishExceptionMetric(request.getActionInvocationPoint(), e, e.getErrorCode());
+ logUnhandledError(e.getMessage(), request, e);
+
+ return ProgressEvent.defaultFailureHandler(e, e.getErrorCode());
+ } catch (final AmazonServiceException | AwsServiceException e) {
+ if ((e instanceof AwsServiceException && ((AwsServiceException) e).statusCode() == HttpStatusCode.BAD_REQUEST)
+ || (e instanceof AmazonServiceException
+ && ((AmazonServiceException) e).getStatusCode() == HttpStatusCode.BAD_REQUEST)) {
+ this.log(String.format("%s [%s] call with invalid request to downstream service", request.getHookTypeName(),
+ request.getActionInvocationPoint()));
+ publishExceptionMetric(request.getActionInvocationPoint(), e, HandlerErrorCode.InvalidRequest);
+
+ return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.InvalidRequest);
+ } else if ((e instanceof AwsServiceException && ((AwsServiceException) e).isThrottlingException())
+ || (e instanceof AmazonServiceException && RetryUtils.isThrottlingException((AmazonServiceException) e))) {
+ this.log(String.format("%s [%s] call throttled by downstream service", request.getHookTypeName(),
+ request.getActionInvocationPoint()));
+ publishExceptionMetric(request.getActionInvocationPoint(), e, HandlerErrorCode.Throttling);
+
+ return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.Throttling);
+ } else if ((e instanceof AwsServiceException
+ && HttpStatusFamily.of(((AwsServiceException) e).statusCode()) == HttpStatusFamily.SERVER_ERROR)
+ || (e instanceof AmazonServiceException
+ && HttpStatusFamily.of(((AmazonServiceException) e).getStatusCode()) == HttpStatusFamily.SERVER_ERROR)) {
+ publishExceptionMetric(request.getActionInvocationPoint(), e, HandlerErrorCode.ServiceInternalError);
+ logUnhandledError("An unknown downstream service error occurred", request, e);
+
+ return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.ServiceInternalError);
+ } else {
+ publishExceptionMetric(request.getActionInvocationPoint(), e, HandlerErrorCode.GeneralServiceException);
+ logUnhandledError("A downstream service error occurred", request, e);
+
+ return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.GeneralServiceException);
+ }
+ } catch (final Throwable e) {
+ publishExceptionMetric(request.getActionInvocationPoint(), e, HandlerErrorCode.InternalFailure);
+ logUnhandledError("An unknown error occurred ", request, e);
+
+ return ProgressEvent.defaultFailureHandler(e, HandlerErrorCode.InternalFailure);
+ } finally {
+ Date endTime = Date.from(Instant.now());
+ metricsPublisherProxy.publishDurationMetric(Instant.now(), request.getActionInvocationPoint(),
+ (endTime.getTime() - startTime.getTime()));
+ }
+ }
+
+ private HookProgressEvent createProgressResponse(final ProgressEvent progressEvent,
+ final HookInvocationRequest request) {
+ final HookProgressEvent response = new HookProgressEvent<>();
+ response.setHookStatus(getHookStatus(progressEvent.getStatus()));
+ response.setErrorCode(progressEvent.getErrorCode());
+ response.setMessage(progressEvent.getMessage());
+ response.setResult(progressEvent.getResult());
+ response.setCallbackContext(progressEvent.getCallbackContext());
+ response.setCallbackDelaySeconds(progressEvent.getCallbackDelaySeconds());
+ if (request != null) {
+ response.setClientRequestToken(request.getClientRequestToken());
+ }
+
+ return response;
+ }
+
+ private void writeResponse(final OutputStream outputStream, final HookProgressEvent response) throws IOException {
+ String output = this.serializer.serialize(response);
+ outputStream.write(output.getBytes(StandardCharsets.UTF_8));
+ outputStream.flush();
+ }
+
+ /**
+ * Transforms the incoming request to the subset of typed models which the
+ * handler implementor needs
+ *
+ * @param request The request as passed from the caller (e.g; CloudFormation)
+ * which contains additional context to inform the LambdaWrapper
+ * itself, and is not needed by the handler implementations
+ * @return A converted HookHandlerRequest model
+ */
+ protected abstract HookHandlerRequest transform(HookInvocationRequest request) throws IOException;
+
+ /**
+ * Handler implementation should implement this method to provide the schema for
+ * validation
+ *
+ * @return An JSONObject of the hook schema for the provider
+ */
+ protected abstract JSONObject provideHookSchemaJSONObject();
+
+ /**
+ * Implemented by the handler package as the key entry point.
+ *
+ * @param proxy Amazon webservice proxy to inject credentials correctly.
+ * @param request incoming request for the call
+ * @param invocationPoint which invocation point the hook is invoked at
+ * @param callbackContext the callback context to handle reentrant calls
+ * @return progress event indicating complete, in progress with delay callback
+ * or failed state
+ * @throws Exception propagate any unexpected errors
+ */
+ public abstract ProgressEvent invokeHandler(AmazonWebServicesClientProxy proxy,
+ HookHandlerRequest request,
+ HookInvocationPoint invocationPoint,
+ CallbackT callbackContext,
+ ConfigurationT typeConfiguration)
+ throws Exception;
+
+ /*
+ * null-safe exception metrics delivery
+ */
+ private void publishExceptionMetric(final HookInvocationPoint invocationPoint,
+ final Throwable ex,
+ final HandlerErrorCode handlerErrorCode) {
+ if (this.metricsPublisherProxy != null) {
+ this.metricsPublisherProxy.publishExceptionMetric(Instant.now(), invocationPoint, ex, handlerErrorCode);
+ } else if (platformLoggerProxy != null) {
+ platformLoggerProxy.log(ex.toString());
+ } else {
+ LOG.error(ex.toString());
+ }
+ }
+
+ /*
+ * null-safe exception metrics delivery
+ */
+ private void publishExceptionCodeAndCountMetrics(final HookInvocationPoint invocationPoint,
+ final HandlerErrorCode handlerErrorCode) {
+ if (this.metricsPublisherProxy != null) {
+ this.metricsPublisherProxy.publishExceptionByErrorCodeAndCountBulkMetrics(Instant.now(), invocationPoint,
+ handlerErrorCode);
+ }
+ }
+
+ private Credentials processCredentials(final String rawCredentials) {
+ if (rawCredentials == null) {
+ return null;
+ }
+
+ if (this.cipher != null) {
+ return this.cipher.decryptCredentials(rawCredentials);
+ }
+
+ // Attempt to deserialize credentials if they are not encrypted.
+ // This would happen while running local contract tests
+ try {
+ return this.serializer.deserialize(rawCredentials, new TypeReference() {
+ });
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private HookStatus getHookStatus(final OperationStatus status) {
+ if (status == OperationStatus.PENDING) {
+ return HookStatus.PENDING;
+ } else if (status == OperationStatus.IN_PROGRESS) {
+ return HookStatus.IN_PROGRESS;
+ } else if (status == OperationStatus.SUCCESS) {
+ return HookStatus.SUCCESS;
+ } else {
+ return HookStatus.FAILED;
+ }
+ }
+
+ /**
+ * null-safe logger redirect
+ *
+ * @param message A string containing the event to log.
+ */
+ private void log(final String message) {
+ if (this.loggerProxy != null) {
+ this.loggerProxy.log(String.format("%s%n", message));
+ } else if (platformLoggerProxy != null) {
+ platformLoggerProxy.log(message);
+ } else {
+ LOG.info(message);
+ }
+ }
+
+ private void logError(final String message) {
+ if (this.loggerProxy != null) {
+ this.loggerProxy.log(String.format("%s%n", message));
+ } else if (platformLoggerProxy != null) {
+ platformLoggerProxy.log(message);
+ } else {
+ LOG.error(message);
+ }
+ }
+
+ protected abstract TypeReference> getTypeReference();
+
+ protected abstract TypeReference getModelTypeReference();
+
+ protected void scrubFiles() {
+ try {
+ FileUtils.cleanDirectory(FileUtils.getTempDirectory());
+ } catch (IOException e) {
+ log(e.getMessage());
+ publishExceptionMetric(null, new FileScrubberException(e), HandlerErrorCode.InternalFailure);
+ }
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/HookExecutableWrapper.java b/src/main/java/software/amazon/cloudformation/HookExecutableWrapper.java
new file mode 100644
index 00000000..bd389d52
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/HookExecutableWrapper.java
@@ -0,0 +1,67 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.cloudformation.encryption.Cipher;
+import software.amazon.cloudformation.exceptions.TerminalException;
+import software.amazon.cloudformation.injection.CredentialsProvider;
+import software.amazon.cloudformation.loggers.CloudWatchLogPublisher;
+import software.amazon.cloudformation.loggers.JavaLogPublisher;
+import software.amazon.cloudformation.loggers.LogPublisher;
+import software.amazon.cloudformation.metrics.MetricsPublisher;
+import software.amazon.cloudformation.resource.SchemaValidator;
+import software.amazon.cloudformation.resource.Serializer;
+
+public abstract class HookExecutableWrapper
+ extends HookAbstractWrapper {
+ private Logger platformLogger = LoggerFactory.getLogger("GLOBAL");
+
+ public HookExecutableWrapper() {
+ super();
+ }
+
+ /*
+ * This .ctor provided for testing
+ */
+ public HookExecutableWrapper(final CredentialsProvider providerCredentialsProvider,
+ final CloudWatchLogPublisher providerEventsLogger,
+ final LogPublisher platformEventsLogger,
+ final MetricsPublisher platformMetricsPublisher,
+ final MetricsPublisher providerMetricsPublisher,
+ final SchemaValidator validator,
+ final Serializer serializer,
+ final SdkHttpClient httpClient,
+ final Cipher cipher) {
+ super(providerCredentialsProvider, providerEventsLogger, platformEventsLogger, platformMetricsPublisher,
+ providerMetricsPublisher, validator, serializer, httpClient, cipher);
+ }
+
+ public void handleRequest(final InputStream inputStream, final OutputStream outputStream) throws IOException,
+ TerminalException {
+
+ if (platformLogPublisher == null) {
+ platformLogPublisher = new JavaLogPublisher(platformLogger);
+ }
+ this.platformLoggerProxy.addLogPublisher(platformLogPublisher);
+ processRequest(inputStream, outputStream);
+ outputStream.flush();
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/HookInvocationPoint.java b/src/main/java/software/amazon/cloudformation/HookInvocationPoint.java
new file mode 100644
index 00000000..719b4541
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/HookInvocationPoint.java
@@ -0,0 +1,43 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation;
+
+public enum HookInvocationPoint {
+ CREATE_PRE_PROVISION("PreCreate"),
+ CREATE_POST_PROVISION("PostCreate"),
+ UPDATE_PRE_PROVISION("PreUpdate"),
+ UPDATE_POST_PROVISION("PostUpdate"),
+ DELETE_PRE_PROVISION("PreDelete"),
+ DELETE_POST_PROVISION("PostDelete");
+
+ private final String shortName;
+
+ HookInvocationPoint(String shortName) {
+ this.shortName = shortName;
+ }
+
+ public String getShortName() {
+ return this.shortName;
+ }
+
+ public static HookInvocationPoint fromShortName(String shortName) {
+ for (HookInvocationPoint e : HookInvocationPoint.values()) {
+ if (e.shortName.equalsIgnoreCase(shortName)) {
+ return e;
+ }
+ }
+ throw new IllegalArgumentException("No HookInvocationPoint with short name " + shortName + " found.");
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/HookLambdaWrapper.java b/src/main/java/software/amazon/cloudformation/HookLambdaWrapper.java
new file mode 100644
index 00000000..ab106848
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/HookLambdaWrapper.java
@@ -0,0 +1,68 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.cloudformation.encryption.Cipher;
+import software.amazon.cloudformation.exceptions.TerminalException;
+import software.amazon.cloudformation.injection.CredentialsProvider;
+import software.amazon.cloudformation.loggers.CloudWatchLogPublisher;
+import software.amazon.cloudformation.loggers.LambdaLogPublisher;
+import software.amazon.cloudformation.loggers.LogPublisher;
+import software.amazon.cloudformation.metrics.MetricsPublisher;
+import software.amazon.cloudformation.resource.SchemaValidator;
+import software.amazon.cloudformation.resource.Serializer;
+
+public abstract class HookLambdaWrapper
+ extends HookAbstractWrapper implements RequestStreamHandler {
+
+ public HookLambdaWrapper() {
+ super();
+ }
+
+ /*
+ * This .ctor provided for testing
+ */
+ public HookLambdaWrapper(final CredentialsProvider providerCredentialsProvider,
+ final CloudWatchLogPublisher providerEventsLogger,
+ final LogPublisher platformEventsLogger,
+ final MetricsPublisher platformMetricsPublisher,
+ final MetricsPublisher providerMetricsPublisher,
+ final SchemaValidator validator,
+ final Serializer serializer,
+ final SdkHttpClient httpClient,
+ final Cipher cipher) {
+ super(providerCredentialsProvider, providerEventsLogger, platformEventsLogger, platformMetricsPublisher,
+ providerMetricsPublisher, validator, serializer, httpClient, cipher);
+ }
+
+ @Override
+ public void handleRequest(final InputStream inputStream, final OutputStream outputStream, final Context context)
+ throws IOException,
+ TerminalException {
+
+ if (platformLogPublisher == null) {
+ platformLogPublisher = new LambdaLogPublisher(context.getLogger());
+ }
+ this.platformLoggerProxy.addLogPublisher(platformLogPublisher);
+ processRequest(inputStream, outputStream);
+ outputStream.close();
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/encryption/Cipher.java b/src/main/java/software/amazon/cloudformation/encryption/Cipher.java
new file mode 100644
index 00000000..bc2fee26
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/encryption/Cipher.java
@@ -0,0 +1,23 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.encryption;
+
+import software.amazon.cloudformation.proxy.Credentials;
+
+public interface Cipher {
+
+ Credentials decryptCredentials(String encryptedCredentials);
+
+}
diff --git a/src/main/java/software/amazon/cloudformation/encryption/KMSCipher.java b/src/main/java/software/amazon/cloudformation/encryption/KMSCipher.java
new file mode 100644
index 00000000..fbf2ea6b
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/encryption/KMSCipher.java
@@ -0,0 +1,106 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.encryption;
+
+import com.amazonaws.ClientConfiguration;
+import com.amazonaws.PredefinedClientConfigurations;
+import com.amazonaws.auth.AWSSessionCredentialsProvider;
+import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider;
+import com.amazonaws.encryptionsdk.AwsCrypto;
+import com.amazonaws.encryptionsdk.CommitmentPolicy;
+import com.amazonaws.encryptionsdk.CryptoResult;
+import com.amazonaws.encryptionsdk.MasterKeyProvider;
+import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
+import com.amazonaws.encryptionsdk.kms.KmsMasterKey;
+import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
+import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
+import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import org.bouncycastle.util.encoders.Base64;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.cloudformation.exceptions.EncryptionException;
+import software.amazon.cloudformation.proxy.Credentials;
+import software.amazon.cloudformation.resource.Serializer;
+
+public class KMSCipher implements Cipher {
+ private static final int STS_CONNECTION_TIMEOUT_MILLIS = 10000;
+ private static final int STS_CONNECTION_TTL_MILLIS = 60000;
+ private static final int STS_CLIENT_EXECUTION_TIMEOUT_MILLIS = 10000;
+ private static final int STS_REQUEST_TIMEOUT_MILLIS = 10000;
+ private static final int STS_SOCKET_TIMEOUT_MILLIS = 10000;
+ private static final int STS_MAX_ERROR_RETRY = 3;
+
+ private final AwsCrypto cryptoHelper;
+ private final MasterKeyProvider kmsKeyProvider;
+ private final Serializer serializer;
+ private final TypeReference credentialsTypeReference;
+
+ public KMSCipher(final String encryptionKeyArn,
+ final String encryptionKeyRole) {
+ final String region = SdkSystemSetting.AWS_REGION.getStringValue().map(Object::toString).orElse("us-east-1");
+
+ final ClientConfiguration clientConfiguration = PredefinedClientConfigurations.defaultConfig()
+ .withConnectionTimeout(STS_CONNECTION_TIMEOUT_MILLIS).withConnectionTTL(STS_CONNECTION_TTL_MILLIS)
+ .withClientExecutionTimeout(STS_CLIENT_EXECUTION_TIMEOUT_MILLIS).withRequestTimeout(STS_REQUEST_TIMEOUT_MILLIS)
+ .withSocketTimeout(STS_SOCKET_TIMEOUT_MILLIS).withMaxErrorRetry(STS_MAX_ERROR_RETRY);
+
+ final AWSSecurityTokenService stsClient = AWSSecurityTokenServiceClientBuilder.standard()
+ .withClientConfiguration(clientConfiguration).withRegion(region).build();
+
+ final AWSSessionCredentialsProvider assumeRoleSessionCredentialsProvider = getAssumeRoleSessionCredentialProvider(
+ encryptionKeyRole, stsClient);
+
+ this.kmsKeyProvider = KmsMasterKeyProvider.builder().withCredentials(assumeRoleSessionCredentialsProvider)
+ .withDefaultRegion(region).buildStrict(encryptionKeyArn);
+
+ this.cryptoHelper = AwsCrypto.builder().withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt).build();
+ this.serializer = new Serializer();
+ this.credentialsTypeReference = getCredentialsTypeReference();
+ }
+
+ // constructor for unit testing
+ public KMSCipher(final AwsCrypto cryptoHelper,
+ final MasterKeyProvider kmsKeyProvider) {
+ this.kmsKeyProvider = kmsKeyProvider;
+ this.cryptoHelper = cryptoHelper;
+ this.serializer = new Serializer();
+ this.credentialsTypeReference = getCredentialsTypeReference();
+ }
+
+ @Override
+ public Credentials decryptCredentials(final String encryptedCredentials) {
+ try {
+ final CryptoResult result = cryptoHelper.decryptData(kmsKeyProvider, Base64.decode(encryptedCredentials));
+ return serializer.deserialize(new String(result.getResult(), StandardCharsets.UTF_8), this.credentialsTypeReference);
+ } catch (final IOException | AwsCryptoException e) {
+ throw new EncryptionException("Failed to decrypt credentials.", e);
+ }
+ }
+
+ private static TypeReference getCredentialsTypeReference() {
+ return new TypeReference() {
+ };
+ }
+
+ private STSAssumeRoleSessionCredentialsProvider
+ getAssumeRoleSessionCredentialProvider(final String encryptionKeyRole, final AWSSecurityTokenService stsClient) {
+ return new STSAssumeRoleSessionCredentialsProvider.Builder(encryptionKeyRole, UUID.randomUUID().toString())
+ .withStsClient(stsClient).build();
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/exceptions/CfnHandlerInternalFailureException.java b/src/main/java/software/amazon/cloudformation/exceptions/CfnHandlerInternalFailureException.java
new file mode 100644
index 00000000..2eafc15d
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/exceptions/CfnHandlerInternalFailureException.java
@@ -0,0 +1,31 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.exceptions;
+
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+
+public class CfnHandlerInternalFailureException extends BaseHandlerException {
+
+ private static final long serialVersionUID = -1646136434112354328L;
+ private static final HandlerErrorCode ERROR_CODE = HandlerErrorCode.HandlerInternalFailure;
+
+ public CfnHandlerInternalFailureException() {
+ this(null);
+ }
+
+ public CfnHandlerInternalFailureException(final Throwable cause) {
+ super(ERROR_CODE.getMessage(), cause, ERROR_CODE);
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/exceptions/CfnNonCompliantException.java b/src/main/java/software/amazon/cloudformation/exceptions/CfnNonCompliantException.java
new file mode 100644
index 00000000..241f2f6b
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/exceptions/CfnNonCompliantException.java
@@ -0,0 +1,38 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.exceptions;
+
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+
+public class CfnNonCompliantException extends BaseHandlerException {
+
+ private static final long serialVersionUID = -1646136434112354328L;
+ private static final HandlerErrorCode ERROR_CODE = HandlerErrorCode.NonCompliant;
+
+ public CfnNonCompliantException(final Throwable cause) {
+ super(cause, ERROR_CODE);
+ }
+
+ public CfnNonCompliantException(final String hookTypeName,
+ final String reason) {
+ this(hookTypeName, reason, null);
+ }
+
+ public CfnNonCompliantException(final String hookTypeName,
+ final String reason,
+ final Throwable cause) {
+ super(String.format(ERROR_CODE.getMessage(), hookTypeName, reason), cause, ERROR_CODE);
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/exceptions/CfnUnknownException.java b/src/main/java/software/amazon/cloudformation/exceptions/CfnUnknownException.java
new file mode 100644
index 00000000..b72866cd
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/exceptions/CfnUnknownException.java
@@ -0,0 +1,31 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.exceptions;
+
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+
+public class CfnUnknownException extends BaseHandlerException {
+
+ private static final long serialVersionUID = -1646136434112354328L;
+ private static final HandlerErrorCode ERROR_CODE = HandlerErrorCode.Unknown;
+
+ public CfnUnknownException() {
+ this(null);
+ }
+
+ public CfnUnknownException(final Throwable cause) {
+ super(ERROR_CODE.getMessage(), cause, ERROR_CODE);
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/exceptions/EncryptionException.java b/src/main/java/software/amazon/cloudformation/exceptions/EncryptionException.java
new file mode 100644
index 00000000..0633895c
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/exceptions/EncryptionException.java
@@ -0,0 +1,32 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.exceptions;
+
+public class EncryptionException extends RuntimeException {
+ private static final long serialVersionUID = -5820555650294338667L;
+
+ public EncryptionException(String message,
+ Throwable cause) {
+ super(message, cause);
+ }
+
+ public EncryptionException(String message) {
+ super(message);
+ }
+
+ protected EncryptionException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/exceptions/UnsupportedTargetException.java b/src/main/java/software/amazon/cloudformation/exceptions/UnsupportedTargetException.java
new file mode 100644
index 00000000..9e53c604
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/exceptions/UnsupportedTargetException.java
@@ -0,0 +1,34 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.exceptions;
+
+public class UnsupportedTargetException extends CfnInvalidRequestException {
+
+ private static final long serialVersionUID = -1646136434112354328L;
+ private static final String ERROR_MESSAGE = "Unsupported target: [%s]";
+
+ public UnsupportedTargetException(final Throwable cause) {
+ super(cause);
+ }
+
+ public UnsupportedTargetException(final String targetTypeName) {
+ this(targetTypeName, null);
+ }
+
+ public UnsupportedTargetException(final String targetTypeName,
+ final Throwable cause) {
+ super(String.format(ERROR_MESSAGE, targetTypeName), cause);
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/metrics/HookMetricsPublisherImpl.java b/src/main/java/software/amazon/cloudformation/metrics/HookMetricsPublisherImpl.java
new file mode 100644
index 00000000..6f50da41
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/metrics/HookMetricsPublisherImpl.java
@@ -0,0 +1,174 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.metrics;
+
+import com.google.common.collect.Sets;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashSet;
+import software.amazon.awssdk.core.SdkSystemSetting;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.cloudwatch.CloudWatchClient;
+import software.amazon.awssdk.services.cloudwatch.model.Dimension;
+import software.amazon.awssdk.services.cloudwatch.model.MetricDatum;
+import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest;
+import software.amazon.awssdk.services.cloudwatch.model.StandardUnit;
+import software.amazon.cloudformation.HookInvocationPoint;
+import software.amazon.cloudformation.injection.CloudWatchProvider;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+import software.amazon.cloudformation.proxy.Logger;
+
+public class HookMetricsPublisherImpl extends MetricsPublisher {
+ private static final String DEFAULT_REGION = "us-east-1";
+
+ private CloudWatchProvider cloudWatchProvider;
+ private Logger loggerProxy;
+ private String awsAccountId;
+ private CloudWatchClient cloudWatchClient;
+
+ public HookMetricsPublisherImpl(final CloudWatchProvider cloudWatchProvider,
+ final Logger loggerProxy,
+ final String awsAccountId,
+ final String hookTypeName) {
+ super(hookTypeName);
+ this.cloudWatchProvider = cloudWatchProvider;
+ this.loggerProxy = loggerProxy;
+ this.awsAccountId = awsAccountId;
+ }
+
+ public HookMetricsPublisherImpl(final Logger loggerProxy,
+ final String awsAccountId,
+ final String hookTypeName) {
+ super(hookTypeName);
+ this.loggerProxy = loggerProxy;
+ this.awsAccountId = awsAccountId;
+ this.cloudWatchClient = createClient();
+ }
+
+ @Override
+ public void refreshClient() {
+ if (cloudWatchProvider != null) {
+ this.cloudWatchClient = cloudWatchProvider.get();
+ }
+ }
+
+ private String getHookTypeName() {
+ return this.typeName;
+ }
+
+ @Override
+ public void publishExceptionMetric(final Instant timestamp,
+ final HookInvocationPoint invocationPoint,
+ final Throwable e,
+ final HandlerErrorCode handlerErrorCode) {
+ publishBulkMetrics(MetricDatum.builder().timestamp(timestamp).metricName(Metric.METRIC_NAME_HANDLER_EXCEPTION)
+ .unit(StandardUnit.COUNT).value(1.0)
+ .dimensions(Sets.newHashSet(
+ Dimension.builder().name(Metric.DIMENSION_KEY_INVOCATION_POINT_TYPE)
+ .value(invocationPoint == null ? "NO_INVOCATION_POINT" : invocationPoint.name()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_EXCEPTION_TYPE).value(e.getClass().toString()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_HOOK_TYPE).value(this.getHookTypeName()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_HANDLER_ERROR_CODE).value(handlerErrorCode.name()).build()))
+ .build());
+ }
+
+ @Override
+ public void publishProviderLogDeliveryExceptionMetric(final Instant timestamp, final Throwable e) {
+ publishBulkMetrics(
+ MetricDatum.builder().metricName(Metric.METRIC_NAME_HANDLER_EXCEPTION).unit(StandardUnit.COUNT).value(1.0)
+ .dimensions(Sets.newHashSet(
+ Dimension.builder().name(Metric.DIMENSION_KEY_INVOCATION_POINT_TYPE).value("HookLogDelivery").build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_EXCEPTION_TYPE).value(e.getClass().toString()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_HOOK_TYPE).value(this.getHookTypeName()).build()))
+ .timestamp(timestamp).build());
+ }
+
+ @Override
+ public void publishInvocationMetric(final Instant timestamp, final HookInvocationPoint invocationPoint) {
+ publishBulkMetrics(
+ MetricDatum.builder().metricName(Metric.METRIC_NAME_HANDLER_INVOCATION_COUNT).unit(StandardUnit.COUNT).value(1.0)
+ .dimensions(Sets.newHashSet(
+ Dimension.builder().name(Metric.DIMENSION_KEY_INVOCATION_POINT_TYPE)
+ .value(invocationPoint == null ? "NO_INVOCATION_POINT" : invocationPoint.name()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_HOOK_TYPE).value(this.getHookTypeName()).build()))
+ .timestamp(timestamp).build());
+ }
+
+ @Override
+ public void
+ publishDurationMetric(final Instant timestamp, final HookInvocationPoint invocationPoint, final long milliseconds) {
+ publishBulkMetrics(MetricDatum.builder().metricName(Metric.METRIC_NAME_HANDLER_DURATION).unit(StandardUnit.MILLISECONDS)
+ .value((double) milliseconds)
+ .dimensions(Sets.newHashSet(
+ Dimension.builder().name(Metric.DIMENSION_KEY_INVOCATION_POINT_TYPE)
+ .value(invocationPoint == null ? "NO_INVOCATION_POINT" : invocationPoint.name()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_HOOK_TYPE).value(this.getHookTypeName()).build()))
+ .timestamp(timestamp).build());
+ }
+
+ @Override
+ public void publishExceptionByErrorCodeAndCountBulkMetrics(final Instant timestamp,
+ final HookInvocationPoint invocationPoint,
+ final HandlerErrorCode handlerErrorCode) {
+ Collection bulkData = new HashSet<>();
+
+ // By Error Code dimensions
+ EnumSet.allOf(HandlerErrorCode.class).forEach(errorCode -> {
+ bulkData.add(MetricDatum.builder().metricName(Metric.METRIC_NAME_HANDLER_EXCEPTION_BY_ERROR_CODE)
+ .unit(StandardUnit.COUNT).value(errorCode == handlerErrorCode ? 1.0 : 0.0)
+ .dimensions(Sets.newHashSet(
+ Dimension.builder().name(Metric.DIMENSION_KEY_INVOCATION_POINT_TYPE)
+ .value(invocationPoint == null ? "NO_INVOCATION_POINT" : invocationPoint.name()).build(),
+ Dimension.builder().name(Metric.DIMENSION_KEY_HANDLER_ERROR_CODE).value(errorCode.name()).build()))
+ .timestamp(timestamp).build());
+ });
+
+ // By Count dimensions
+ bulkData.add(MetricDatum.builder().metricName(Metric.METRIC_NAME_HANDLER_EXCEPTION_BY_EXCEPTION_COUNT)
+ .unit(StandardUnit.COUNT).value(handlerErrorCode == null ? 0.0 : 1.0)
+ .dimensions(Dimension.builder().name(Metric.DIMENSION_KEY_INVOCATION_POINT_TYPE)
+ .value(invocationPoint == null ? "NO_INVOCATION_POINT" : invocationPoint.name()).build())
+ .timestamp(timestamp).build());
+
+ publishBulkMetrics(bulkData.toArray(new MetricDatum[0]));
+ }
+
+ private void publishBulkMetrics(final MetricDatum... metricData) {
+ assert cloudWatchClient != null : "CloudWatchEventsClient was not initialised. You must call refreshClient() first.";
+
+ PutMetricDataRequest putMetricDataRequest = PutMetricDataRequest.builder()
+ .namespace(String.format("%s/%s/%s", Metric.METRIC_NAMESPACE_ROOT, awsAccountId, namespace)).metricData(metricData)
+ .build();
+
+ try {
+ this.cloudWatchClient.putMetricData(putMetricDataRequest);
+ } catch (final Exception e) {
+ log(String.format("An error occurred while publishing metrics: %s", e.getMessage()));
+ }
+ }
+
+ private void log(final String message) {
+ if (loggerProxy != null) {
+ loggerProxy.log(String.format("%s%n", message));
+ }
+ }
+
+ private CloudWatchClient createClient() {
+ final String region = SdkSystemSetting.AWS_REGION.getStringValue().map(Object::toString).orElse(DEFAULT_REGION);
+ return CloudWatchClient.builder().region(Region.of(region)).build();
+ }
+
+}
diff --git a/src/main/java/software/amazon/cloudformation/metrics/Metric.java b/src/main/java/software/amazon/cloudformation/metrics/Metric.java
index 80b4b14a..7b76c3d0 100644
--- a/src/main/java/software/amazon/cloudformation/metrics/Metric.java
+++ b/src/main/java/software/amazon/cloudformation/metrics/Metric.java
@@ -24,8 +24,10 @@ public class Metric {
public static final String METRIC_NAME_HANDLER_INVOCATION_COUNT = "HandlerInvocationCount";
public static final String DIMENSION_KEY_ACTION_TYPE = "Action";
+ public static final String DIMENSION_KEY_INVOCATION_POINT_TYPE = "InvocationPoint";
public static final String DIMENSION_KEY_EXCEPTION_TYPE = "ExceptionType";
public static final String DIMENSION_KEY_RESOURCE_TYPE = "ResourceType";
+ public static final String DIMENSION_KEY_HOOK_TYPE = "HookType";
public static final String DIMENSION_KEY_HANDLER_ERROR_CODE = "HandlerErrorCode";
private Metric() {
diff --git a/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisher.java b/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisher.java
index fc2b1322..55a06e88 100644
--- a/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisher.java
+++ b/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisher.java
@@ -16,16 +16,17 @@
import java.time.Instant;
import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.HookInvocationPoint;
import software.amazon.cloudformation.proxy.HandlerErrorCode;
public abstract class MetricsPublisher {
- protected String resourceTypeName;
- protected String resourceNamespace;
+ protected String typeName;
+ protected String namespace;
- public MetricsPublisher(final String resourceTypeName) {
- this.resourceTypeName = resourceTypeName;
- this.resourceNamespace = resourceTypeName.replace("::", "/");
+ public MetricsPublisher(final String typeName) {
+ this.typeName = typeName;
+ this.namespace = typeName.replace("::", "/");
}
/**
@@ -42,17 +43,35 @@ public void publishExceptionMetric(final Instant timestamp,
final HandlerErrorCode handlerErrorCode) {
}
+ public void publishExceptionMetric(final Instant timestamp,
+ final HookInvocationPoint invocationPoint,
+ final Throwable e,
+ final HandlerErrorCode handlerErrorCode) {
+ }
+
public void publishExceptionByErrorCodeAndCountBulkMetrics(final Instant timestamp,
final Action action,
final HandlerErrorCode handlerErrorCode) {
}
+ public void publishExceptionByErrorCodeAndCountBulkMetrics(final Instant timestamp,
+ final HookInvocationPoint invocationPoint,
+ final HandlerErrorCode handlerErrorCode) {
+ }
+
public void publishInvocationMetric(final Instant timestamp, final Action action) {
}
+ public void publishInvocationMetric(final Instant timestamp, final HookInvocationPoint invocationPoint) {
+ }
+
public void publishDurationMetric(final Instant timestamp, final Action action, final long milliseconds) {
}
+ public void
+ publishDurationMetric(final Instant timestamp, final HookInvocationPoint invocationPoint, final long milliseconds) {
+ }
+
public void publishProviderLogDeliveryExceptionMetric(final Instant timestamp, final Throwable exception) {
}
}
diff --git a/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisherImpl.java b/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisherImpl.java
index 8e76929c..e3822a80 100644
--- a/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisherImpl.java
+++ b/src/main/java/software/amazon/cloudformation/metrics/MetricsPublisherImpl.java
@@ -49,7 +49,7 @@ public void refreshClient() {
}
private String getResourceTypeName() {
- return this.resourceTypeName;
+ return this.typeName;
}
@Override
@@ -131,9 +131,8 @@ private void publishBulkMetrics(final MetricDatum... metricData) {
assert cloudWatchClient != null : "CloudWatchEventsClient was not initialised. You must call refreshClient() first.";
try {
- this.cloudWatchClient.putMetricData(
- PutMetricDataRequest.builder().namespace(String.format("%s/%s", Metric.METRIC_NAMESPACE_ROOT, resourceNamespace))
- .metricData(metricData).build());
+ this.cloudWatchClient.putMetricData(PutMetricDataRequest.builder()
+ .namespace(String.format("%s/%s", Metric.METRIC_NAMESPACE_ROOT, namespace)).metricData(metricData).build());
} catch (final Exception e) {
log(String.format("An error occurred while publishing metrics: %s", e.getMessage()));
}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/ExceptionMessages.java b/src/main/java/software/amazon/cloudformation/proxy/ExceptionMessages.java
index ccf5af9a..5c6931d4 100644
--- a/src/main/java/software/amazon/cloudformation/proxy/ExceptionMessages.java
+++ b/src/main/java/software/amazon/cloudformation/proxy/ExceptionMessages.java
@@ -30,6 +30,9 @@ final class ExceptionMessages {
static final String SERVICE_LIMIT_EXCEEDED = "Limit exceeded for resource of type '%s'. Reason: %s";
static final String THROTTLING = "Rate exceeded for operation '%s'.";
static final String INVALID_TYPECONFIGURATION = "Invalid TypeConfiguration provided for type '%s'. Reason: %s";
+ static final String HANDLER_INTERNAL_FAILURE = "Internal error occurred in the handler.";
+ static final String NON_COMPLIANT = "Hook of type '%s' returned a Non-Complaint status. Reason %s";
+ static final String UNKNOWN = "Unknown error occurred.";
private ExceptionMessages() {
}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/HandlerErrorCode.java b/src/main/java/software/amazon/cloudformation/proxy/HandlerErrorCode.java
index f9c45f3e..1c4061b3 100644
--- a/src/main/java/software/amazon/cloudformation/proxy/HandlerErrorCode.java
+++ b/src/main/java/software/amazon/cloudformation/proxy/HandlerErrorCode.java
@@ -104,7 +104,27 @@ public enum HandlerErrorCode {
/**
* typeConfiguration is null or required typeConfiguration property is null
*/
- InvalidTypeConfiguration(ExceptionMessages.INVALID_TYPECONFIGURATION);
+ InvalidTypeConfiguration(ExceptionMessages.INVALID_TYPECONFIGURATION),
+
+ /**
+ * an internal error occurred within the handler, such as an NPE, etc.
+ * (Terminal)
+ */
+ HandlerInternalFailure(ExceptionMessages.HANDLER_INTERNAL_FAILURE),
+
+ /**
+ * the specified target of the Hook is in a non-compliant state. Only applicable
+ * to Hook type handlers (Terminal) Hook Handlers return this error when the
+ * hook's compliance checks have failed.
+ */
+ NonCompliant(ExceptionMessages.NON_COMPLIANT),
+
+ /**
+ * the Hook has returned a failure for an Unknown reason. Only applicable to
+ * Hook type handlers (terminal) Hook Handlers can return this when a hook has
+ * failed for a reason other than non-compliance
+ */
+ Unknown(ExceptionMessages.UNKNOWN);
@Getter
private String message;
diff --git a/src/main/java/software/amazon/cloudformation/proxy/MetricsPublisherProxy.java b/src/main/java/software/amazon/cloudformation/proxy/MetricsPublisherProxy.java
index 57fa9824..b99d128c 100644
--- a/src/main/java/software/amazon/cloudformation/proxy/MetricsPublisherProxy.java
+++ b/src/main/java/software/amazon/cloudformation/proxy/MetricsPublisherProxy.java
@@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.List;
import software.amazon.cloudformation.Action;
+import software.amazon.cloudformation.HookInvocationPoint;
import software.amazon.cloudformation.metrics.MetricsPublisher;
public class MetricsPublisherProxy {
@@ -35,6 +36,14 @@ public void publishExceptionMetric(final Instant timestamp,
.forEach(metricsPublisher -> metricsPublisher.publishExceptionMetric(timestamp, action, e, handlerErrorCode));
}
+ public void publishExceptionMetric(final Instant timestamp,
+ final HookInvocationPoint invocationPoint,
+ final Throwable e,
+ final HandlerErrorCode handlerErrorCode) {
+ metricsPublishers.stream().forEach(
+ metricsPublisher -> metricsPublisher.publishExceptionMetric(timestamp, invocationPoint, e, handlerErrorCode));
+ }
+
public void publishExceptionByErrorCodeAndCountBulkMetrics(final Instant timestamp,
final Action action,
final HandlerErrorCode handlerErrorCode) {
@@ -42,15 +51,33 @@ public void publishExceptionByErrorCodeAndCountBulkMetrics(final Instant timesta
.publishExceptionByErrorCodeAndCountBulkMetrics(timestamp, action, handlerErrorCode));
}
+ public void publishExceptionByErrorCodeAndCountBulkMetrics(final Instant timestamp,
+ final HookInvocationPoint invocationPoint,
+ final HandlerErrorCode handlerErrorCode) {
+ metricsPublishers.stream().forEach(metricsPublishers -> metricsPublishers
+ .publishExceptionByErrorCodeAndCountBulkMetrics(timestamp, invocationPoint, handlerErrorCode));
+ }
+
public void publishInvocationMetric(final Instant timestamp, final Action action) {
metricsPublishers.stream().forEach(metricsPublisher -> metricsPublisher.publishInvocationMetric(timestamp, action));
}
+ public void publishInvocationMetric(final Instant timestamp, final HookInvocationPoint invocationPoint) {
+ metricsPublishers.stream()
+ .forEach(metricsPublisher -> metricsPublisher.publishInvocationMetric(timestamp, invocationPoint));
+ }
+
public void publishDurationMetric(final Instant timestamp, final Action action, final long milliseconds) {
metricsPublishers.stream()
.forEach(metricsPublisher -> metricsPublisher.publishDurationMetric(timestamp, action, milliseconds));
}
+ public void
+ publishDurationMetric(final Instant timestamp, final HookInvocationPoint invocationPoint, final long milliseconds) {
+ metricsPublishers.stream()
+ .forEach(metricsPublisher -> metricsPublisher.publishDurationMetric(timestamp, invocationPoint, milliseconds));
+ }
+
public void publishProviderLogDeliveryExceptionMetric(final Instant timestamp, final Throwable exception) {
metricsPublishers.stream()
.forEach(metricsPublisher -> metricsPublisher.publishProviderLogDeliveryExceptionMetric(timestamp, exception));
diff --git a/src/main/java/software/amazon/cloudformation/proxy/ProgressEvent.java b/src/main/java/software/amazon/cloudformation/proxy/ProgressEvent.java
index dae27752..0b900ea2 100644
--- a/src/main/java/software/amazon/cloudformation/proxy/ProgressEvent.java
+++ b/src/main/java/software/amazon/cloudformation/proxy/ProgressEvent.java
@@ -45,6 +45,12 @@ public class ProgressEvent {
*/
private String message;
+ /**
+ * The optional data to be returned by a hook handler as part of the invocation
+ * result. This field is ignored for resource handlers.
+ */
+ private String result;
+
/**
* The callback context is an arbitrary datum which the handler can return in an
* IN_PROGRESS event to allow the passing through of additional state or
@@ -150,6 +156,17 @@ public static ProgressEvent success
return event;
}
+ public static ProgressEvent success(ResourceT model, CallbackT cxt, String message) {
+ return success(model, cxt, message, null);
+ }
+
+ public static ProgressEvent success(ResourceT model, CallbackT cxt, String message, String result) {
+ return ProgressEvent.builder().resourceModel(model).callbackContext(cxt).message(message)
+ .result(result).status(OperationStatus.SUCCESS).build();
+ }
+
public ProgressEvent
onSuccess(Function, ProgressEvent> func) {
return (status != null && status == OperationStatus.SUCCESS) ? func.apply(this) : this;
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookContext.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookContext.java
new file mode 100644
index 00000000..4835fd18
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookContext.java
@@ -0,0 +1,52 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import software.amazon.cloudformation.HookInvocationPoint;
+import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
+
+/**
+ * This interface describes the hook context object for the hook invocation
+ * request passed to the implementor.
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder(toBuilder = true)
+public class HookContext {
+ private String awsAccountId;
+ private String stackId;
+ private String changeSetId;
+ private String hookTypeName;
+ private String hookTypeVersion;
+ private HookInvocationPoint invocationPoint;
+ private String targetName;
+ private String targetType;
+ private String targetLogicalId;
+ private HookTargetModel targetModel;
+
+ public TargetModelT getTargetModel(final Class clazz) {
+ return HookTargetModel.of(this.targetModel, clazz);
+ }
+
+ public TargetModelT getTargetModel(final TypeReference typeReference) {
+ return HookTargetModel.of(this.targetModel, typeReference);
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookHandlerRequest.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookHandlerRequest.java
new file mode 100644
index 00000000..3c61cad4
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookHandlerRequest.java
@@ -0,0 +1,35 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * This interface describes the request object for the hook invocation request
+ * passed to the implementor. It is transformed from an instance of
+ * HookInvocationRequest by the HookLambdaWrapper to only items of concern
+ *
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder(toBuilder = true)
+public class HookHandlerRequest {
+ private String clientRequestToken;
+ private HookContext hookContext;
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookHandlerTestPayload.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookHandlerTestPayload.java
new file mode 100644
index 00000000..8adbb996
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookHandlerTestPayload.java
@@ -0,0 +1,41 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import software.amazon.cloudformation.HookInvocationPoint;
+import software.amazon.cloudformation.proxy.Credentials;
+
+/**
+ * This POJO is for the test entrypoint that bypasses the wrapper for direct
+ * testing.
+ *
+ * @param Type of hook type configuration model
+ * @param Type of callback data to be passed on re-invocation
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder(toBuilder = true)
+public class HookHandlerTestPayload {
+ private Credentials credentials;
+ private HookInvocationPoint actionInvocationPoint;
+ private HookHandlerRequest request;
+ private CallbackT callbackContext;
+ private ConfigurationT typeConfiguration;
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookInvocationRequest.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookInvocationRequest.java
new file mode 100644
index 00000000..d1422e1e
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookInvocationRequest.java
@@ -0,0 +1,41 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import software.amazon.cloudformation.HookInvocationPoint;
+
+/**
+ * This interface describes the request object for the hook invocation request.
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder(toBuilder = true)
+public class HookInvocationRequest {
+ private String clientRequestToken;
+ private String awsAccountId;
+ private String stackId;
+ private String changeSetId;
+ private String hookTypeName;
+ private String hookTypeVersion;
+ private ConfigurationT hookModel;
+ private HookInvocationPoint actionInvocationPoint;
+ private HookRequestData requestData;
+ private HookRequestContext requestContext;
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookProgressEvent.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookProgressEvent.java
new file mode 100644
index 00000000..51beccfc
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookProgressEvent.java
@@ -0,0 +1,159 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.util.function.Function;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import software.amazon.cloudformation.proxy.HandlerErrorCode;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class HookProgressEvent {
+ /**
+ * The bearerToken is used to report progress back to CloudFormation and is
+ * passed back to CloudFormation
+ */
+ private String clientRequestToken;
+
+ /**
+ * The status indicates whether the handler has reached a terminal state or is
+ * still computing and requires more time to complete
+ */
+ private HookStatus hookStatus;
+
+ /**
+ * If OperationStatus is FAILED, an error code should be provided
+ */
+ private HandlerErrorCode errorCode;
+
+ /**
+ * The handler can (and should) specify a contextual information message which
+ * can be shown to callers to indicate the nature of a progress transition or
+ * callback delay; for example a message indicating "propagating to edge"
+ */
+ private String message;
+
+ /**
+ * The callback context is an arbitrary datum which the handler can return in an
+ * IN_PROGRESS event to allow the passing through of additional state or
+ * metadata between subsequent retries; for example to pass through a Resource
+ * identifier which can be used to continue polling for stabilization
+ */
+ private CallbackT callbackContext;
+
+ /**
+ * A callback will be scheduled with an initial delay of no less than the number
+ * of seconds specified in the progress event.
+ */
+ private int callbackDelaySeconds;
+
+ /**
+ * The optional data to be returned by hook handler as part of the invocation
+ * result.
+ */
+ private String result;
+
+ /**
+ * Convenience method for constructing a FAILED response
+ *
+ * @param e actual service exception
+ * @param handlerErrorCode error code to return to CloudFormation
+ * @param the type for the callback context
+ * @return {@link HookProgressEvent} failed status code
+ */
+ public static HookProgressEvent defaultFailureHandler(final Throwable e,
+ final HandlerErrorCode handlerErrorCode) {
+ return failed(null, handlerErrorCode, e.getMessage(), null);
+ }
+
+ public static <
+ CallbackT> HookProgressEvent failed(CallbackT cxt, HandlerErrorCode code, String message, String data) {
+ return HookProgressEvent.builder().callbackContext(cxt).errorCode(code).message(message).result(data)
+ .hookStatus(HookStatus.FAILED).build();
+ }
+
+ /**
+ * Convenience method for constructing a IN_PROGRESS response
+ *
+ * @param cxt callback context
+ * @param delaySeconds how much time to wait before calling back the handler
+ * @param The type for the callback context
+ * @return {@link HookProgressEvent} with {@link HookStatus#IN_PROGRESS}
+ */
+ public static HookProgressEvent defaultProgressHandler(CallbackT cxt, int delaySeconds) {
+ return progress(cxt, null, null, delaySeconds);
+ }
+
+ public static <
+ CallbackT> HookProgressEvent progress(CallbackT cxt, String message, String data, int delaySeconds) {
+ return HookProgressEvent.builder().callbackContext(cxt).callbackDelaySeconds(delaySeconds).message(message)
+ .result(data).hookStatus(HookStatus.IN_PROGRESS).build();
+ }
+
+ /**
+ * Convenience method for constructing a SUCCESS response
+ *
+ * @param The type for the callback context
+ * @return {@link HookProgressEvent} with {@link HookStatus#SUCCESS} indicating
+ * successful completion for operation
+ */
+ public static HookProgressEvent defaultCompleteHandler() {
+ return complete(null, null, null);
+ }
+
+ public static HookProgressEvent complete(CallbackT cxt, String message, String data) {
+ return HookProgressEvent.builder().callbackContext(cxt).message(message).result(data)
+ .hookStatus(HookStatus.SUCCESS).build();
+ }
+
+ public HookProgressEvent onComplete(Function, HookProgressEvent> func) {
+ return (hookStatus != null && hookStatus == HookStatus.SUCCESS) ? func.apply(this) : this;
+ }
+
+ @JsonIgnore
+ public boolean isFailed() {
+ return hookStatus == HookStatus.FAILED;
+ }
+
+ @JsonIgnore
+ public boolean isInProgress() {
+ return hookStatus == HookStatus.IN_PROGRESS;
+ }
+
+ @JsonIgnore
+ public boolean canContinueProgress() {
+ return hookStatus == HookStatus.IN_PROGRESS && callbackDelaySeconds == 0;
+ }
+
+ public HookProgressEvent then(Function, HookProgressEvent> func) {
+ return canContinueProgress() ? func.apply(this) : this;
+ }
+
+ @JsonIgnore
+ public boolean isComplete() {
+ return hookStatus == HookStatus.SUCCESS;
+ }
+
+ @JsonIgnore
+ public boolean isInProgressCallbackDelay() {
+ return hookStatus == HookStatus.IN_PROGRESS && callbackDelaySeconds > 0;
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestContext.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestContext.java
new file mode 100644
index 00000000..2b17fa46
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestContext.java
@@ -0,0 +1,34 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class HookRequestContext {
+ /**
+ * The number of times the handler has been invoked (including current)
+ */
+ private int invocation;
+
+ /**
+ * Custom context object to enable handlers to process re-invocation
+ */
+ private CallbackT callbackContext;
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java
new file mode 100644
index 00000000..53541480
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java
@@ -0,0 +1,37 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder(toBuilder = true)
+public class HookRequestData {
+ private String targetName;
+ private String targetType;
+ private String targetLogicalId;
+ private Map targetModel;
+ private String callerCredentials;
+ private String providerCredentials;
+ private String providerLogGroupName;
+ private String hookEncryptionKeyArn;
+ private String hookEncryptionKeyRole;
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java
new file mode 100644
index 00000000..1fd22521
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java
@@ -0,0 +1,22 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook;
+
+public enum HookStatus {
+ PENDING,
+ IN_PROGRESS,
+ SUCCESS,
+ FAILED
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/GenericHookTargetModel.java b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/GenericHookTargetModel.java
new file mode 100644
index 00000000..a4a34052
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/GenericHookTargetModel.java
@@ -0,0 +1,72 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook.targetmodel;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+/**
+ * This class is a concrete agnostic target model that is the default
+ * implementation used when the type of the Hook's target is not known. Since it
+ * is possible for Hooks to support multiple target types, the type of the
+ * hook's target is not known until it receives a request during runtime.
+ *
+ * The properties of the Hook's target are stored internally in a Map which can
+ * be accessed using the {@link HookTargetModel} accessor methods
+ */
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@ToString(callSuper = true)
+@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
+@JsonDeserialize(as = GenericHookTargetModel.class)
+public class GenericHookTargetModel extends HookTargetModel {
+ private static final TypeReference extends HookTarget> TARGET_REFERENCE = new TypeReference() {
+ };
+ private static final TypeReference MODEL_REFERENCE = new TypeReference() {
+ };
+
+ @JsonAnySetter
+ private void setTargetModelProperties(final String key, final Object value) {
+ setTargetModelProperty(key, value);
+ }
+
+ @JsonAnyGetter
+ public Map getTargetModelProperties() {
+ return super.getTargetModel();
+ }
+
+ @Override
+ protected HookTargetType getHookTargetType() {
+ return HookTargetType.GENERIC;
+ }
+
+ @Override
+ protected TypeReference extends HookTarget> getHookTargetTypeReference() {
+ return TARGET_REFERENCE;
+ }
+
+ @Override
+ protected TypeReference getTargetModelTypeReference() {
+ return MODEL_REFERENCE;
+ }
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTarget.java b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTarget.java
new file mode 100644
index 00000000..5b47a9ff
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTarget.java
@@ -0,0 +1,23 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook.targetmodel;
+
+public interface HookTarget {
+
+ Object get(String key);
+
+ HookTargetType getHookTargetType();
+
+}
diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModel.java b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModel.java
new file mode 100644
index 00000000..b5dfcafe
--- /dev/null
+++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModel.java
@@ -0,0 +1,171 @@
+/*
+* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the "License").
+* You may not use this file except in compliance with the License.
+* A copy of the License is located at
+*
+* http://aws.amazon.com/apache2.0
+*
+* or in the "license" file accompanying this file. This file is distributed
+* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+* express or implied. See the License for the specific language governing
+* permissions and limitations under the License.
+*/
+package software.amazon.cloudformation.proxy.hook.targetmodel;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.CheckForNull;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.NonNull;
+import lombok.Setter;
+import org.json.JSONObject;
+import software.amazon.cloudformation.resource.Serializer;
+
+@Data
+@Setter(AccessLevel.PRIVATE)
+@JsonDeserialize(as = GenericHookTargetModel.class)
+public abstract class HookTargetModel {
+ private static final TypeReference