From aec8b997f56058df38c684b30c4c2ef601d36a79 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Wed, 6 Mar 2024 15:08:33 -0500 Subject: [PATCH 01/14] Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. --- src/cloudformation_cli_python_lib/utils.py | 24 ++++++ src/setup.py | 1 + tests/lib/hook_test.py | 98 +++++++++++++++++++++- 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/cloudformation_cli_python_lib/utils.py b/src/cloudformation_cli_python_lib/utils.py index f610487a..32a6be96 100644 --- a/src/cloudformation_cli_python_lib/utils.py +++ b/src/cloudformation_cli_python_lib/utils.py @@ -2,6 +2,7 @@ from dataclasses import dataclass, field, fields import json +import requests # type: ignore from datetime import date, datetime, time from typing import ( Any, @@ -25,6 +26,9 @@ HookInvocationPoint, ) +HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME = "targetModel" +HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS = 10 + class KitchenSinkEncoder(json.JSONEncoder): def default(self, o): # type: ignore # pylint: disable=method-hidden @@ -214,6 +218,7 @@ class HookRequestData: targetType: str targetLogicalId: str targetModel: Mapping[str, Any] + payload: Optional[str] = None callerCredentials: Optional[Credentials] = None providerCredentials: Optional[Credentials] = None providerLogGroupName: Optional[str] = None @@ -234,6 +239,17 @@ def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HookRequestData": if creds: cred_data = json.loads(creds) setattr(req_data, key, Credentials(**cred_data)) + + if req_data.is_hook_invocation_payload_remote(): + response = requests.get( + req_data.payload, + timeout=HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS, + ) + if response.status_code == 200: + setattr( + req_data, HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME, response.json() + ) + return req_data def serialize(self) -> Mapping[str, Any]: @@ -247,6 +263,14 @@ def serialize(self) -> Mapping[str, Any]: if value is not None } + def is_hook_invocation_payload_remote(self) -> bool: + if ( + not self.targetModel and self.payload + ): # pylint: disable=simplifiable-if-statement + return True + + return False + @dataclass class HookInvocationRequest: diff --git a/src/setup.py b/src/setup.py index 5c8fea27..d5dce5c7 100644 --- a/src/setup.py +++ b/src/setup.py @@ -16,6 +16,7 @@ install_requires=[ "boto3>=1.10.20", 'dataclasses;python_version<"3.7"', + "requests>=2.22", ], license="Apache License 2.0", classifiers=[ diff --git a/tests/lib/hook_test.py b/tests/lib/hook_test.py index 65f993af..29ba9547 100644 --- a/tests/lib/hook_test.py +++ b/tests/lib/hook_test.py @@ -14,10 +14,15 @@ OperationStatus, ProgressEvent, ) -from cloudformation_cli_python_lib.utils import Credentials, HookInvocationRequest +from cloudformation_cli_python_lib.utils import ( + Credentials, + HookInvocationRequest, + HookRequestData, +) import json from datetime import datetime +from typing import Any, Mapping from unittest.mock import Mock, call, patch, sentinel ENTRYPOINT_PAYLOAD = { @@ -50,6 +55,34 @@ "hookModel": sentinel.type_configuration, } +STACK_LEVEL_HOOK_ENTRYPOINT_PAYLOAD = { + "awsAccountId": "123456789012", + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe", + "region": "us-east-1", + "actionInvocationPoint": "CREATE_PRE_PROVISION", + "hookTypeName": "AWS::Test::TestHook", + "hookTypeVersion": "1.0", + "requestContext": { + "invocation": 1, + "callbackContext": {}, + }, + "requestData": { + "callerCredentials": '{"accessKeyId": "IASAYK835GAIFHAHEI23", "secretAccessKey": "66iOGPN5LnpZorcLr8Kh25u8AbjHVllv5poh2O0", "sessionToken": "lameHS2vQOknSHWhdFYTxm2eJc1JMn9YBNI4nV4mXue945KPL6DHfW8EsUQT5zwssYEC1NvYP9yD6Y5s5lKR3chflOHPFsIe6eqg"}', # noqa: B950 + "providerCredentials": '{"accessKeyId": "HDI0745692Y45IUTYR78", "secretAccessKey": "4976TUYVI2345GW87ERYG823RF87GY9EIUH452I3", "sessionToken": "842HYOFIQAEUDF78R8T7IU43HSADYGIFHBJSDHFA87SDF9PYvN1CEYASDUYFT5TQ97YASIHUDFAIUEYRISDKJHFAYSUDTFSDFADS"}', # noqa: B950 + "providerLogGroupName": "providerLoggingGroupName", + "targetName": "STACK", + "targetType": "STACK", + "targetLogicalId": "myStack", + "hookEncryptionKeyArn": None, + "hookEncryptionKeyRole": None, + "payload": "https://someS3PresignedURL", + "targetModel": {}, + }, + "stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e" + "722ae60-fe62-11e8-9a0e-0ae8cc519968", + "hookModel": sentinel.type_configuration, +} + TYPE_NAME = "Test::Foo::Bar" @@ -456,3 +489,66 @@ def test_test_entrypoint_success(): ) def test_get_hook_status(operation_status, hook_status): assert hook_status == Hook._get_hook_status(operation_status) + + +def test__hook_request_data_remote_payload(): + non_remote_input = HookRequestData( + targetName="someTargetName", + targetType="someTargetModel", + targetLogicalId="someTargetLogicalId", + targetModel={"resourceProperties": {"propKeyA": "propValueA"}}, + ) + assert non_remote_input.is_hook_invocation_payload_remote() is False + + non_remote_input_1 = HookRequestData( + targetName="someTargetName", + targetType="someTargetModel", + targetLogicalId="someTargetLogicalId", + targetModel={"resourceProperties": {"propKeyA": "propValueA"}}, + payload="https://someUrl", + ) + assert non_remote_input_1.is_hook_invocation_payload_remote() is False + + remote_input = HookRequestData( + targetName="someTargetName", + targetType="someTargetModel", + targetLogicalId="someTargetLogicalId", + targetModel={}, + payload="https://someUrl", + ) + assert remote_input.is_hook_invocation_payload_remote() is True + + +def test__test_stack_level_hook_input(hook): + hook = Hook(TYPE_NAME, Mock()) + + with patch("cloudformation_cli_python_lib.utils.requests.get") as mock_requests_lib: + mock_requests_lib.return_value = MockResponse(200, {"foo": "bar"}) + _, _, _, req = hook._parse_request(STACK_LEVEL_HOOK_ENTRYPOINT_PAYLOAD) + + assert req.requestData.targetName == "STACK" + assert req.requestData.targetType == "STACK" + assert req.requestData.targetLogicalId == "myStack" + assert req.requestData.targetModel == {"foo": "bar"} + + +def test__test_stack_level_hook_input_failed_s3_download(hook): + hook = Hook(TYPE_NAME, Mock()) + + with patch("cloudformation_cli_python_lib.utils.requests.get") as mock_requests_lib: + mock_requests_lib.return_value = MockResponse(404, {"foo": "bar"}) + _, _, _, req = hook._parse_request(STACK_LEVEL_HOOK_ENTRYPOINT_PAYLOAD) + + assert req.requestData.targetName == "STACK" + assert req.requestData.targetType == "STACK" + assert req.requestData.targetLogicalId == "myStack" + assert req.requestData.targetModel == {} + + +@dataclass +class MockResponse: + status_code: int + _json: Mapping[str, Any] + + def json(self) -> Mapping[str, Any]: + return self._json From 13500e448bd317cbcb59f87f4eda21cb69e66d03 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Wed, 13 Mar 2024 10:07:59 -0400 Subject: [PATCH 02/14] Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. --- src/cloudformation_cli_python_lib/utils.py | 32 +++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/cloudformation_cli_python_lib/utils.py b/src/cloudformation_cli_python_lib/utils.py index 32a6be96..a34b598f 100644 --- a/src/cloudformation_cli_python_lib/utils.py +++ b/src/cloudformation_cli_python_lib/utils.py @@ -4,6 +4,7 @@ import json import requests # type: ignore from datetime import date, datetime, time +from requests.adapters import HTTPAdapter # type: ignore from typing import ( Any, Callable, @@ -15,6 +16,7 @@ Type, Union, ) +from urllib3 import Retry # type: ignore from .exceptions import InvalidRequest from .interface import ( @@ -28,6 +30,9 @@ HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME = "targetModel" HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS = 10 +HOOK_REMOTE_PAYLOAD_RETRY_LIMIT = 3 +HOOK_REMOTE_PAYLOAD_RETRY_BACKOFF_FACTOR = 1 +HOOK_REMOTE_PAYLOAD_RETRY_STATUSES = [500, 502, 503, 504] class KitchenSinkEncoder(json.JSONEncoder): @@ -241,15 +246,28 @@ def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HookRequestData": setattr(req_data, key, Credentials(**cred_data)) if req_data.is_hook_invocation_payload_remote(): - response = requests.get( - req_data.payload, - timeout=HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS, - ) - if response.status_code == 200: - setattr( - req_data, HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME, response.json() + with requests.Session() as s: + retries = Retry( + total=HOOK_REMOTE_PAYLOAD_RETRY_LIMIT, + backoff_factor=HOOK_REMOTE_PAYLOAD_RETRY_BACKOFF_FACTOR, + status_forcelist=HOOK_REMOTE_PAYLOAD_RETRY_STATUSES, ) + s.mount("http://", HTTPAdapter(max_retries=retries)) + s.mount("https://", HTTPAdapter(max_retries=retries)) + + response = s.get( + req_data.payload, + timeout=HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS, + ) + + if response.status_code == 200: + setattr( + req_data, + HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME, + response.json(), + ) + return req_data def serialize(self) -> Mapping[str, Any]: From 62852691e270a0668e72e5b2e1b38122487562fd Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Thu, 4 Apr 2024 13:12:47 -0400 Subject: [PATCH 03/14] Skip stack level hook for stack if prior stack level change set hook succeeded For stack level hooks, customers are able to return a new status that allow stack level hooks that execute against a stack to skip with a successful status. The idea is that if a stack hook invoked against a change set succeeds, there is no need to invoke against the stack once the change set is processed. --- src/cloudformation_cli_python_lib/interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cloudformation_cli_python_lib/interface.py b/src/cloudformation_cli_python_lib/interface.py index 2c8e19f0..c67aa126 100644 --- a/src/cloudformation_cli_python_lib/interface.py +++ b/src/cloudformation_cli_python_lib/interface.py @@ -46,6 +46,7 @@ class OperationStatus(str, _AutoName): PENDING = auto() IN_PROGRESS = auto() SUCCESS = auto() + CHANGE_SET_SUCCESS_SKIP_STACK_HOOK = auto() FAILED = auto() @@ -53,6 +54,7 @@ class HookStatus(str, _AutoName): PENDING = auto() IN_PROGRESS = auto() SUCCESS = auto() + CHANGE_SET_SUCCESS_SKIP_STACK_HOOK = auto() FAILED = auto() From 0014ea2a34cd686d2e13315e9241b7ad04a810fc Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Sun, 30 Jun 2024 10:52:50 -0400 Subject: [PATCH 04/14] testing --- .pre-commit-config.yaml | 20 ++++++++------------ src/cloudformation_cli_python_lib/utils.py | 22 ++++++++++++++++------ tests/lib/hook_test.py | 16 ++++++++++------ tests/lib/interface_test.py | 9 +++++---- tests/lib/log_delivery_test.py | 4 ++-- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39b220bf..db453ec9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,23 +61,19 @@ repos: - --no-warn-unused-ignores # https://github.com/python/mypy/issues/8823, https://github.com/python/mypy/issues/8990 - repo: local hooks: - - id: pylint-local - name: pylint-local - description: Run pylint in the local virtualenv - entry: pylint "setup.py" "src/" "python/" "tests/" - language: system - # ignore all files, run on hard-coded modules instead - pass_filenames: false - always_run: true + # - id: pylint-local + # name: pylint-local + # description: Run pylint in the local virtualenv + # entry: pylint "setup.py" "src/" "python/" "tests/" + # language: system + # # ignore all files, run on hard-coded modules instead + # pass_filenames: false + # always_run: true - id: pytest-local name: pytest-local description: Run pytest in the local virtualenv entry: > pytest - --cov-report=term - --cov-report=html - --cov="rpdk.python" - --cov="cloudformation_cli_python_lib" --durations=5 "tests/" language: system diff --git a/src/cloudformation_cli_python_lib/utils.py b/src/cloudformation_cli_python_lib/utils.py index a34b598f..a243c63c 100644 --- a/src/cloudformation_cli_python_lib/utils.py +++ b/src/cloudformation_cli_python_lib/utils.py @@ -29,6 +29,7 @@ ) HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME = "targetModel" +HOOK_REQUEST_DATA_PAYLOAD_FIELD_NAME = "payload" HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS = 10 HOOK_REMOTE_PAYLOAD_RETRY_LIMIT = 3 HOOK_REMOTE_PAYLOAD_RETRY_BACKOFF_FACTOR = 1 @@ -218,11 +219,11 @@ def serialize(self) -> Mapping[str, Any]: @dataclass -class HookRequestData: +class HookRequestDataFooBar: targetName: str targetType: str targetLogicalId: str - targetModel: Mapping[str, Any] + targetModel: Optional[Mapping[str, Any]] = None payload: Optional[str] = None callerCredentials: Optional[Credentials] = None providerCredentials: Optional[Credentials] = None @@ -235,8 +236,10 @@ def __init__(self, **kwargs: Any) -> None: setattr(self, k, v) @classmethod - def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HookRequestData": - req_data = HookRequestData(**json_data) + def deserialize( + cls, json_data: MutableMapping[str, Any] + ) -> "HookRequestDataFooBar": + req_data = HookRequestDataFooBar(**json_data) for key in json_data: if not key.endswith("Credentials"): continue @@ -262,6 +265,7 @@ def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HookRequestData": ) if response.status_code == 200: + # pass setattr( req_data, HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME, @@ -286,6 +290,12 @@ def is_hook_invocation_payload_remote(self) -> bool: not self.targetModel and self.payload ): # pylint: disable=simplifiable-if-statement return True + # return True + # if ( + # not hasattr(self, HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME) + # and hasattr(self, HOOK_REQUEST_DATA_PAYLOAD_FIELD_NAME) + # ): # pylint: disable=simplifiable-if-statement + # return True return False @@ -297,7 +307,7 @@ class HookInvocationRequest: hookTypeName: str hookTypeVersion: str actionInvocationPoint: str - requestData: HookRequestData + requestData: HookRequestDataFooBar clientRequestToken: str changeSetId: Optional[str] = None hookModel: Optional[Mapping[str, Any]] = None @@ -312,7 +322,7 @@ def __init__(self, **kwargs: Any) -> None: @classmethod def deserialize(cls, json_data: MutableMapping[str, Any]) -> Any: event = HookInvocationRequest(**json_data) - event.requestData = HookRequestData.deserialize( + event.requestData = HookRequestDataFooBar.deserialize( json_data.get("requestData", {}) ) event.requestContext = HookRequestContext.deserialize( diff --git a/tests/lib/hook_test.py b/tests/lib/hook_test.py index 29ba9547..176ce253 100644 --- a/tests/lib/hook_test.py +++ b/tests/lib/hook_test.py @@ -17,7 +17,7 @@ from cloudformation_cli_python_lib.utils import ( Credentials, HookInvocationRequest, - HookRequestData, + HookRequestDataFooBar, ) import json @@ -492,7 +492,7 @@ def test_get_hook_status(operation_status, hook_status): def test__hook_request_data_remote_payload(): - non_remote_input = HookRequestData( + non_remote_input = HookRequestDataFooBar( targetName="someTargetName", targetType="someTargetModel", targetLogicalId="someTargetLogicalId", @@ -500,7 +500,7 @@ def test__hook_request_data_remote_payload(): ) assert non_remote_input.is_hook_invocation_payload_remote() is False - non_remote_input_1 = HookRequestData( + non_remote_input_1 = HookRequestDataFooBar( targetName="someTargetName", targetType="someTargetModel", targetLogicalId="someTargetLogicalId", @@ -509,7 +509,7 @@ def test__hook_request_data_remote_payload(): ) assert non_remote_input_1.is_hook_invocation_payload_remote() is False - remote_input = HookRequestData( + remote_input = HookRequestDataFooBar( targetName="someTargetName", targetType="someTargetModel", targetLogicalId="someTargetLogicalId", @@ -522,7 +522,9 @@ def test__hook_request_data_remote_payload(): def test__test_stack_level_hook_input(hook): hook = Hook(TYPE_NAME, Mock()) - with patch("cloudformation_cli_python_lib.utils.requests.get") as mock_requests_lib: + with patch( + "cloudformation_cli_python_lib.utils.requests.Session.get" + ) as mock_requests_lib: mock_requests_lib.return_value = MockResponse(200, {"foo": "bar"}) _, _, _, req = hook._parse_request(STACK_LEVEL_HOOK_ENTRYPOINT_PAYLOAD) @@ -535,7 +537,9 @@ def test__test_stack_level_hook_input(hook): def test__test_stack_level_hook_input_failed_s3_download(hook): hook = Hook(TYPE_NAME, Mock()) - with patch("cloudformation_cli_python_lib.utils.requests.get") as mock_requests_lib: + with patch( + "cloudformation_cli_python_lib.utils.requests.Session.get" + ) as mock_requests_lib: mock_requests_lib.return_value = MockResponse(404, {"foo": "bar"}) _, _, _, req = hook._parse_request(STACK_LEVEL_HOOK_ENTRYPOINT_PAYLOAD) diff --git a/tests/lib/interface_test.py b/tests/lib/interface_test.py index 3ab49fd0..455ee458 100644 --- a/tests/lib/interface_test.py +++ b/tests/lib/interface_test.py @@ -185,7 +185,8 @@ def test_hook_progress_event_serialize_to_response_with_error_code(message): } -def test_operation_status_enum_matches_sdk(client): - sdk = set(client.meta.service_model.shape_for("OperationStatus").enum) - enum = set(OperationStatus.__members__) - assert enum == sdk +# add this test back when +# def test_operation_status_enum_matches_sdk(client): +# sdk = set(client.meta.service_model.shape_for("OperationStatus").enum) +# enum = set(OperationStatus.__members__) +# assert enum == sdk diff --git a/tests/lib/log_delivery_test.py b/tests/lib/log_delivery_test.py index 4087369f..66e596f0 100644 --- a/tests/lib/log_delivery_test.py +++ b/tests/lib/log_delivery_test.py @@ -8,7 +8,7 @@ from cloudformation_cli_python_lib.utils import ( HandlerRequest, HookInvocationRequest, - HookRequestData, + HookRequestDataFooBar, RequestData, ) @@ -60,7 +60,7 @@ def make_hook_payload() -> HookInvocationRequest: clientRequestToken=str(uuid4()), hookTypeName="AWS::Test::Hook", hookTypeVersion="3", - requestData=HookRequestData( + requestData=HookRequestDataFooBar( providerLogGroupName="test_group", targetName="AWS::Test::Resource", targetType="RESOURCE", From 2501df8f3e323b2f3fdae704c8b108efa9a73635 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Sun, 30 Jun 2024 11:02:45 -0400 Subject: [PATCH 05/14] testing --- python/rpdk/python/__init__.py | 2 +- src/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/rpdk/python/__init__.py b/python/rpdk/python/__init__.py index e3c4ffd2..cfa2c71a 100644 --- a/python/rpdk/python/__init__.py +++ b/python/rpdk/python/__init__.py @@ -1,5 +1,5 @@ import logging -__version__ = "2.1.9" +__version__ = "2.1.10" logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/src/setup.py b/src/setup.py index 8dddc953..d6af7967 100644 --- a/src/setup.py +++ b/src/setup.py @@ -3,7 +3,7 @@ setup( name="cloudformation-cli-python-lib", - version="2.1.18", + version="2.1.19", description=__doc__, author="Amazon Web Services", author_email="aws-cloudformation-developers@amazon.com", From e94aed5b10b71ffd21e563fcbdc0076baf2e4ab5 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Sun, 30 Jun 2024 11:43:10 -0400 Subject: [PATCH 06/14] testing --- src/cloudformation_cli_python_lib/utils.py | 18 +++++------------- tests/lib/hook_test.py | 8 ++++---- tests/lib/log_delivery_test.py | 4 ++-- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/cloudformation_cli_python_lib/utils.py b/src/cloudformation_cli_python_lib/utils.py index a243c63c..098582d1 100644 --- a/src/cloudformation_cli_python_lib/utils.py +++ b/src/cloudformation_cli_python_lib/utils.py @@ -219,7 +219,7 @@ def serialize(self) -> Mapping[str, Any]: @dataclass -class HookRequestDataFooBar: +class HookRequestData: targetName: str targetType: str targetLogicalId: str @@ -236,10 +236,8 @@ def __init__(self, **kwargs: Any) -> None: setattr(self, k, v) @classmethod - def deserialize( - cls, json_data: MutableMapping[str, Any] - ) -> "HookRequestDataFooBar": - req_data = HookRequestDataFooBar(**json_data) + def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HookRequestData": + req_data = HookRequestData(**json_data) for key in json_data: if not key.endswith("Credentials"): continue @@ -290,12 +288,6 @@ def is_hook_invocation_payload_remote(self) -> bool: not self.targetModel and self.payload ): # pylint: disable=simplifiable-if-statement return True - # return True - # if ( - # not hasattr(self, HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME) - # and hasattr(self, HOOK_REQUEST_DATA_PAYLOAD_FIELD_NAME) - # ): # pylint: disable=simplifiable-if-statement - # return True return False @@ -307,7 +299,7 @@ class HookInvocationRequest: hookTypeName: str hookTypeVersion: str actionInvocationPoint: str - requestData: HookRequestDataFooBar + requestData: HookRequestData clientRequestToken: str changeSetId: Optional[str] = None hookModel: Optional[Mapping[str, Any]] = None @@ -322,7 +314,7 @@ def __init__(self, **kwargs: Any) -> None: @classmethod def deserialize(cls, json_data: MutableMapping[str, Any]) -> Any: event = HookInvocationRequest(**json_data) - event.requestData = HookRequestDataFooBar.deserialize( + event.requestData = HookRequestData.deserialize( json_data.get("requestData", {}) ) event.requestContext = HookRequestContext.deserialize( diff --git a/tests/lib/hook_test.py b/tests/lib/hook_test.py index 176ce253..0bedfcf0 100644 --- a/tests/lib/hook_test.py +++ b/tests/lib/hook_test.py @@ -17,7 +17,7 @@ from cloudformation_cli_python_lib.utils import ( Credentials, HookInvocationRequest, - HookRequestDataFooBar, + HookRequestData, ) import json @@ -492,7 +492,7 @@ def test_get_hook_status(operation_status, hook_status): def test__hook_request_data_remote_payload(): - non_remote_input = HookRequestDataFooBar( + non_remote_input = HookRequestData( targetName="someTargetName", targetType="someTargetModel", targetLogicalId="someTargetLogicalId", @@ -500,7 +500,7 @@ def test__hook_request_data_remote_payload(): ) assert non_remote_input.is_hook_invocation_payload_remote() is False - non_remote_input_1 = HookRequestDataFooBar( + non_remote_input_1 = HookRequestData( targetName="someTargetName", targetType="someTargetModel", targetLogicalId="someTargetLogicalId", @@ -509,7 +509,7 @@ def test__hook_request_data_remote_payload(): ) assert non_remote_input_1.is_hook_invocation_payload_remote() is False - remote_input = HookRequestDataFooBar( + remote_input = HookRequestData( targetName="someTargetName", targetType="someTargetModel", targetLogicalId="someTargetLogicalId", diff --git a/tests/lib/log_delivery_test.py b/tests/lib/log_delivery_test.py index 66e596f0..4087369f 100644 --- a/tests/lib/log_delivery_test.py +++ b/tests/lib/log_delivery_test.py @@ -8,7 +8,7 @@ from cloudformation_cli_python_lib.utils import ( HandlerRequest, HookInvocationRequest, - HookRequestDataFooBar, + HookRequestData, RequestData, ) @@ -60,7 +60,7 @@ def make_hook_payload() -> HookInvocationRequest: clientRequestToken=str(uuid4()), hookTypeName="AWS::Test::Hook", hookTypeVersion="3", - requestData=HookRequestDataFooBar( + requestData=HookRequestData( providerLogGroupName="test_group", targetName="AWS::Test::Resource", targetType="RESOURCE", From 5d42f5ab742cc9ecc19f23d2bb7580aed5af6019 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Sun, 30 Jun 2024 11:44:07 -0400 Subject: [PATCH 07/14] testing --- src/cloudformation_cli_python_lib/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cloudformation_cli_python_lib/utils.py b/src/cloudformation_cli_python_lib/utils.py index 098582d1..a1ce058f 100644 --- a/src/cloudformation_cli_python_lib/utils.py +++ b/src/cloudformation_cli_python_lib/utils.py @@ -29,7 +29,6 @@ ) HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME = "targetModel" -HOOK_REQUEST_DATA_PAYLOAD_FIELD_NAME = "payload" HOOK_REMOTE_PAYLOAD_CONNECT_AND_READ_TIMEOUT_SECONDS = 10 HOOK_REMOTE_PAYLOAD_RETRY_LIMIT = 3 HOOK_REMOTE_PAYLOAD_RETRY_BACKOFF_FACTOR = 1 @@ -263,7 +262,6 @@ def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HookRequestData": ) if response.status_code == 200: - # pass setattr( req_data, HOOK_REQUEST_DATA_TARGET_MODEL_FIELD_NAME, From 88b6713d846276a22d7464a9523990dddb9d9eb2 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Wed, 7 Aug 2024 14:14:08 -0400 Subject: [PATCH 08/14] Add new operation status translation --- src/cloudformation_cli_python_lib/hook.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cloudformation_cli_python_lib/hook.py b/src/cloudformation_cli_python_lib/hook.py index 2d7b40be..29fbb73c 100644 --- a/src/cloudformation_cli_python_lib/hook.py +++ b/src/cloudformation_cli_python_lib/hook.py @@ -293,6 +293,8 @@ def _get_hook_status(operation_status: OperationStatus) -> HookStatus: hook_status = HookStatus.IN_PROGRESS elif operation_status == OperationStatus.SUCCESS: hook_status = HookStatus.SUCCESS + elif operation_status == OperationStatus.CHANGE_SET_SUCCESS_SKIP_STACK_HOOK: + hook_status = HookStatus.CHANGE_SET_SUCCESS_SKIP_STACK_HOOK else: hook_status = HookStatus.FAILED return hook_status From bbaeb0579f7b5ed857c1fd0c3f04df70d2b34338 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 25 Nov 2024 11:14:59 -0500 Subject: [PATCH 09/14] WIP --- .pre-commit-config.yaml | 20 ++++++++++++-------- tests/lib/hook_test.py | 4 ++++ tests/lib/interface_test.py | 11 ++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db453ec9..39b220bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,19 +61,23 @@ repos: - --no-warn-unused-ignores # https://github.com/python/mypy/issues/8823, https://github.com/python/mypy/issues/8990 - repo: local hooks: - # - id: pylint-local - # name: pylint-local - # description: Run pylint in the local virtualenv - # entry: pylint "setup.py" "src/" "python/" "tests/" - # language: system - # # ignore all files, run on hard-coded modules instead - # pass_filenames: false - # always_run: true + - id: pylint-local + name: pylint-local + description: Run pylint in the local virtualenv + entry: pylint "setup.py" "src/" "python/" "tests/" + language: system + # ignore all files, run on hard-coded modules instead + pass_filenames: false + always_run: true - id: pytest-local name: pytest-local description: Run pytest in the local virtualenv entry: > pytest + --cov-report=term + --cov-report=html + --cov="rpdk.python" + --cov="cloudformation_cli_python_lib" --durations=5 "tests/" language: system diff --git a/tests/lib/hook_test.py b/tests/lib/hook_test.py index 0bedfcf0..1ce2fb7b 100644 --- a/tests/lib/hook_test.py +++ b/tests/lib/hook_test.py @@ -485,6 +485,10 @@ def test_test_entrypoint_success(): (OperationStatus.IN_PROGRESS, HookStatus.IN_PROGRESS), (OperationStatus.SUCCESS, HookStatus.SUCCESS), (OperationStatus.FAILED, HookStatus.FAILED), + ( + OperationStatus.CHANGE_SET_SUCCESS_SKIP_STACK_HOOK, + HookStatus.CHANGE_SET_SUCCESS_SKIP_STACK_HOOK, + ), ], ) def test_get_hook_status(operation_status, hook_status): diff --git a/tests/lib/interface_test.py b/tests/lib/interface_test.py index 455ee458..01d9c2a6 100644 --- a/tests/lib/interface_test.py +++ b/tests/lib/interface_test.py @@ -185,8 +185,9 @@ def test_hook_progress_event_serialize_to_response_with_error_code(message): } -# add this test back when -# def test_operation_status_enum_matches_sdk(client): -# sdk = set(client.meta.service_model.shape_for("OperationStatus").enum) -# enum = set(OperationStatus.__members__) -# assert enum == sdk +def test_operation_status_enum_matches_sdk(client): + sdk = set(client.meta.service_model.shape_for("OperationStatus").enum) + enum = set(OperationStatus.__members__) + # CHANGE_SET_SUCCESS_SKIP_STACK_HOOK is a status specific to Hooks + enum.remove("CHANGE_SET_SUCCESS_SKIP_STACK_HOOK") + assert enum == sdk From ada088752d760bbeefa417b1b823d4ce307c9bc4 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 25 Nov 2024 12:02:16 -0500 Subject: [PATCH 10/14] wip --- .pre-commit-config.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39b220bf..dee660fc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,7 +65,9 @@ repos: name: pylint-local description: Run pylint in the local virtualenv entry: pylint "setup.py" "src/" "python/" "tests/" - language: system + language: python + additional_dependencies: + - requests>=2.22 # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true @@ -80,7 +82,9 @@ repos: --cov="cloudformation_cli_python_lib" --durations=5 "tests/" - language: system + language: python + additional_dependencies: + - requests>=2.22 # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true From dfd007490c0db9ddf1f5242b1c5336a4f2575b47 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 25 Nov 2024 12:05:53 -0500 Subject: [PATCH 11/14] wip --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dee660fc..0ee46023 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,6 +68,7 @@ repos: language: python additional_dependencies: - requests>=2.22 + - setuptools # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true @@ -85,6 +86,7 @@ repos: language: python additional_dependencies: - requests>=2.22 + - setuptools # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true From d004bbecff4709308d0326d4d615165e169a3746 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 25 Nov 2024 12:16:16 -0500 Subject: [PATCH 12/14] wip --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ee46023..4947cc86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,7 +68,7 @@ repos: language: python additional_dependencies: - requests>=2.22 - - setuptools + - setuptools>=65.5.0 # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true @@ -86,7 +86,7 @@ repos: language: python additional_dependencies: - requests>=2.22 - - setuptools + - setuptools>=65.5.0 # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true From 117e310b51940582f13f743d4a913c97975e5aec Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 25 Nov 2024 12:24:22 -0500 Subject: [PATCH 13/14] wip --- .pre-commit-config.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4947cc86..39b220bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,10 +65,7 @@ repos: name: pylint-local description: Run pylint in the local virtualenv entry: pylint "setup.py" "src/" "python/" "tests/" - language: python - additional_dependencies: - - requests>=2.22 - - setuptools>=65.5.0 + language: system # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true @@ -83,10 +80,7 @@ repos: --cov="cloudformation_cli_python_lib" --durations=5 "tests/" - language: python - additional_dependencies: - - requests>=2.22 - - setuptools>=65.5.0 + language: system # ignore all files, run on hard-coded modules instead pass_filenames: false always_run: true From 6cca2fc72bcd31afbbb1088ae154c05722e6de23 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 2 Dec 2024 11:24:51 -0500 Subject: [PATCH 14/14] wip --- setup.py | 1 + src/setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 6bc36e9c..af042812 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def find_version(*file_paths): install_requires=[ "cloudformation-cli>=0.2.26", "types-dataclasses>=0.1.5", + "setuptools", ], entry_points={ "rpdk.v1.languages": [ diff --git a/src/setup.py b/src/setup.py index d6af7967..72f56dde 100644 --- a/src/setup.py +++ b/src/setup.py @@ -17,6 +17,7 @@ "boto3>=1.34.6", 'dataclasses;python_version<"3.7"', "requests>=2.22", + "setuptools", ], license="Apache License 2.0", classifiers=[