Skip to content

Commit 8f583c9

Browse files
Add support for Wildcard Hook Targets (#945)
* Added support for full wildcard target matching for HOOK types
1 parent 9f1539a commit 8f583c9

22 files changed

+1689
-678
lines changed

src/rpdk/core/contract/hook_client.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# pylint: disable=R0904
33
# have to skip B404, import_subprocess is required for executing typescript
44
# have to skip B60*, to allow typescript code to be executed using subprocess
5+
import fnmatch
56
import json
67
import logging
78
import re
@@ -98,6 +99,7 @@ def __init__(
9899
self._docker_client = docker.from_env() if self._docker_image else None
99100
self._executable_entrypoint = executable_entrypoint
100101
self._target_info = self._setup_target_info(target_info)
102+
self._resolved_targets = {}
101103

102104
@staticmethod
103105
def _properties_to_paths(schema, key):
@@ -140,12 +142,24 @@ def get_hook_type_name(self):
140142
return self._type_name if self._type_name else self._schema["typeName"]
141143

142144
def get_handler_targets(self, invocation_point):
143-
try:
144-
handlers = self._schema["handlers"]
145-
handler = handlers[generate_handler_name(invocation_point)]
146-
return handler["targetNames"]
147-
except KeyError:
148-
return set()
145+
handler = self._schema["handlers"][generate_handler_name(invocation_point)]
146+
147+
targets = set()
148+
for target_name in handler.get("targetNames", []):
149+
if self._contains_wildcard(target_name):
150+
if target_name not in self._resolved_targets:
151+
self._resolved_targets[target_name] = fnmatch.filter(
152+
self._target_info.keys(), target_name
153+
)
154+
targets.update(self._resolved_targets[target_name])
155+
else:
156+
targets.add(target_name)
157+
158+
return sorted(targets)
159+
160+
@staticmethod
161+
def _contains_wildcard(pattern):
162+
return pattern and ("*" in pattern or "?" in pattern)
149163

150164
@staticmethod
151165
def assert_in_progress(status, response, target=""):
@@ -239,7 +253,7 @@ def _generate_target_example(self, target):
239253
return {}
240254

241255
info = self._target_info.get(target)
242-
if not info.get("SchemaStrategy"):
256+
if not info.get("SchemaStrategy"): # pragma: no cover
243257
# imported here to avoid hypothesis being loaded before pytest is loaded
244258
from .resource_generator import ResourceGenerator
245259

@@ -260,7 +274,7 @@ def _generate_target_update_example(self, target, model):
260274
return {}
261275

262276
info = self._target_info.get(target)
263-
if not info.get("UpdateSchemaStrategy"):
277+
if not info.get("UpdateSchemaStrategy"): # pragma: no cover
264278
# imported here to avoid hypothesis being loaded before pytest is loaded
265279
from .resource_generator import ResourceGenerator
266280

@@ -497,3 +511,11 @@ def call(
497511
status = HookStatus[response["hookStatus"]]
498512

499513
return status, response
514+
515+
def handler_has_wildcard_targets(self, invocation_point):
516+
return any(
517+
self._contains_wildcard(target_name)
518+
for target_name in self._schema["handlers"][
519+
generate_handler_name(invocation_point)
520+
]["targetNames"]
521+
)

src/rpdk/core/contract/interface.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ class HandlerErrorCode(AutoName):
5353
InvalidTypeConfiguration = auto()
5454
HandlerInternalFailure = auto()
5555
NonCompliant = auto()
56+
UnsupportedTarget = auto()
5657
Unknown = auto()

src/rpdk/core/contract/suite/hook/handler_pre_create.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rpdk.core.contract.suite.hook.hook_handler_commons import (
77
test_hook_handlers_failed,
88
test_hook_handlers_success,
9+
test_hook_unsupported_target,
910
)
1011

1112
LOG = logging.getLogger(__name__)
@@ -21,3 +22,8 @@ def contract_pre_create_success(hook_client):
2122
@pytest.mark.create_pre_provision
2223
def contract_pre_create_failed(hook_client):
2324
test_hook_handlers_failed(hook_client, INVOCATION_POINT)
25+
26+
27+
@pytest.mark.create_pre_provision
28+
def contract_pre_create_failed_unsupported_target(hook_client):
29+
test_hook_unsupported_target(hook_client, INVOCATION_POINT)

src/rpdk/core/contract/suite/hook/handler_pre_delete.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rpdk.core.contract.suite.hook.hook_handler_commons import (
77
test_hook_handlers_failed,
88
test_hook_handlers_success,
9+
test_hook_unsupported_target,
910
)
1011

1112
LOG = logging.getLogger(__name__)
@@ -21,3 +22,8 @@ def contract_pre_delete_success(hook_client):
2122
@pytest.mark.delete_pre_provision
2223
def contract_pre_delete_failed(hook_client):
2324
test_hook_handlers_failed(hook_client, INVOCATION_POINT)
25+
26+
27+
@pytest.mark.delete_pre_provision
28+
def contract_pre_delete_failed_unsupported_target(hook_client):
29+
test_hook_unsupported_target(hook_client, INVOCATION_POINT)

src/rpdk/core/contract/suite/hook/handler_pre_update.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rpdk.core.contract.suite.hook.hook_handler_commons import (
77
test_hook_handlers_failed,
88
test_hook_handlers_success,
9+
test_hook_unsupported_target,
910
)
1011

1112
LOG = logging.getLogger(__name__)
@@ -21,3 +22,8 @@ def contract_pre_update_success(hook_client):
2122
@pytest.mark.update_pre_provision
2223
def contract_pre_update_failed(hook_client):
2324
test_hook_handlers_failed(hook_client, INVOCATION_POINT)
25+
26+
27+
@pytest.mark.update_pre_provision
28+
def contract_pre_update_failed_unsupported_target(hook_client):
29+
test_hook_unsupported_target(hook_client, INVOCATION_POINT)

src/rpdk/core/contract/suite/hook/hook_handler_commons.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1+
# pylint: disable=import-outside-toplevel
12
import logging
23

4+
import pytest
5+
36
from rpdk.core.contract.hook_client import HookClient
4-
from rpdk.core.contract.interface import HookStatus
7+
from rpdk.core.contract.interface import HandlerErrorCode, HookStatus
8+
from rpdk.core.contract.suite.contract_asserts_commons import failed_event
59

610
LOG = logging.getLogger(__name__)
711

12+
TARGET_NAME_REGEX = "^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$"
13+
14+
UNSUPPORTED_TARGET_SCHEMA = {
15+
"type": "object",
16+
"properties": {
17+
"id": {"type": "string", "format": "arn"},
18+
"property1": {"type": "string", "pattern": "^[a-zA-Z0-9]{2,26}$"},
19+
"property2": {"type": "integer", "minimum": 1, "maximum": 100},
20+
},
21+
}
22+
823

924
def test_hook_success(hook_client, invocation_point, target, target_model):
1025
if HookClient.is_update_invocation_point(invocation_point):
@@ -66,3 +81,37 @@ def test_hook_handlers_failed(hook_client, invocation_point):
6681
target_model,
6782
) in hook_client.generate_invalid_request_examples(invocation_point):
6883
test_hook_failed(hook_client, invocation_point, target, target_model)
84+
85+
86+
@failed_event(
87+
error_code=HandlerErrorCode.UnsupportedTarget,
88+
msg="A hook handler MUST return FAILED with a UnsupportedTarget error code if the target is not supported",
89+
)
90+
def test_hook_unsupported_target(hook_client, invocation_point):
91+
if not hook_client.handler_has_wildcard_targets(invocation_point):
92+
pytest.skip("No wildcard hook targets. Skipping test.")
93+
94+
# imported here to avoid hypothesis being loaded before pytest is loaded
95+
from ...resource_generator import ResourceGenerator
96+
97+
unsupported_target = ResourceGenerator(
98+
UNSUPPORTED_TARGET_SCHEMA
99+
).generate_schema_strategy(UNSUPPORTED_TARGET_SCHEMA)
100+
101+
target_model = {"resourceProperties": unsupported_target.example()}
102+
if HookClient.is_update_invocation_point(invocation_point):
103+
target_model["previousResourceProperties"] = unsupported_target.example()
104+
target_model["previousResourceProperties"]["id"] = target_model[
105+
"resourceProperties"
106+
]["id"]
107+
108+
_response, error_code = test_hook_failed(
109+
hook_client,
110+
invocation_point,
111+
ResourceGenerator.generate_string_strategy(
112+
{"pattern": TARGET_NAME_REGEX}
113+
).example(),
114+
target_model,
115+
)
116+
117+
return error_code

src/rpdk/core/contract/type_configuration.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ def get_type_configuration():
4141

4242
@staticmethod
4343
def get_hook_configuration():
44+
# pylint: disable=unsubscriptable-object
4445
type_configuration = TypeConfiguration.get_type_configuration()
4546
if type_configuration:
4647
try:
47-
return type_configuration.get("CloudFormationConfiguration", {})[
48+
return type_configuration["CloudFormationConfiguration"][
4849
"HookConfiguration"
49-
]["Properties"]
50+
].get("Properties")
5051
except KeyError as e:
5152
LOG.warning("Hook configuration is invalid")
5253
raise InvalidProjectError("Hook configuration is invalid") from e

src/rpdk/core/data/pytest-contract.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ markers =
1919
delete_pre_provision: preDelete handler related tests.
2020

2121
filterwarnings =
22-
ignore::hypothesis.errors.NonInteractiveExampleWarning:hypothesis
22+
ignore::hypothesis.errors.NonInteractiveExampleWarning

src/rpdk/core/data_loaders.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,18 +399,23 @@ def load_hook_spec(hook_spec_file): # pylint: disable=R # noqa: C901
399399
raise SpecValidationError(str(e)) from e
400400

401401
blocked_handler_permissions = {"cloudformation:RegisterType"}
402-
for handler in hook_spec.get("handlers", []):
403-
for permission in hook_spec.get("handlers", [])[handler]["permissions"]:
402+
for handler in hook_spec.get("handlers", {}).values():
403+
for permission in handler["permissions"]:
404404
if "cloudformation:*" in permission:
405405
raise SpecValidationError(
406406
f"Wildcards for cloudformation are not allowed for hook handler permissions: '{permission}'"
407407
)
408-
409408
if permission in blocked_handler_permissions:
410409
raise SpecValidationError(
411410
f"Permission is not allowed for hook handler permissions: '{permission}'"
412411
)
413412

413+
for target_name in handler["targetNames"]:
414+
if "*?" in target_name:
415+
raise SpecValidationError(
416+
f"Wildcard pattern '*?' is not allowed in target name: '{target_name}'"
417+
)
418+
414419
try:
415420
base_uri = hook_spec["$id"]
416421
except KeyError:

src/rpdk/core/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ class ModelResolverError(RPDKBaseException):
6060

6161
class InvalidFragmentFileError(RPDKBaseException):
6262
pass
63+
64+
65+
class InvalidTypeSchemaError(RPDKBaseException):
66+
pass

0 commit comments

Comments
 (0)