diff --git a/README.md b/README.md index 18311862..cd5ec660 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# DEVELOPER PREVIEW +# AWS CloudFormation Resource Provider Python Plugin -We're excited to share our progress with adding new languages to the CloudFormation CLI! This plugin is an early preview, and not ready for production use. +The CloudFormation CLI (cfn) allows you to author your own resource providers that can be used by CloudFormation. + +This plugin library helps to provide Python runtime bindings for the execution of your providers by CloudFormation. ## AWS CloudFormation Resource Provider Python Plugin @@ -13,18 +15,13 @@ This plugin library helps to provide runtime bindings for the execution of your Installation ------------ -Because this is a developer preview, you can't install it from pypi (the version there will not work) -You can still install the plugin using [pip](https://pypi.org/project/pip/) from GitHub. - ```bash -pip install git+https://github.com/aws-cloudformation/aws-cloudformation-rpdk-python-plugin.git#egg=cloudformation-cli-python-plugin +pip install cloudformation-cli-python-plugin ``` Howto ----- -The support library, `cloudformation-cli-python-lib` must be packaged and present in the project's directory. Packaging can be done by running the `package_lib.sh` script. Example run: - ``` $ cfn init Initializing new project @@ -43,7 +40,6 @@ This is highly recommended unless you are experienced with cross-platform Python packaging. >> y Initialized a new project in <> -$ cp ../cloudformation-cli-python-lib-0.0.1.tar.gz . $ cfn submit --dry-run $ cat < test.json { diff --git a/package_lib.sh b/package_lib.sh deleted file mode 100755 index 5cee15a6..00000000 --- a/package_lib.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -OUT=$(pwd) -cd src/ -python3 setup.py sdist -cp dist/*.tar.gz "$OUT" -rm -r dist/ diff --git a/python/rpdk/python/__init__.py b/python/rpdk/python/__init__.py index 5684f64a..2a5cd013 100644 --- a/python/rpdk/python/__init__.py +++ b/python/rpdk/python/__init__.py @@ -1,5 +1,5 @@ import logging -__version__ = "2.0.0" +__version__ = "2.1.0" logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/python/rpdk/python/data/Python.gitignore b/python/rpdk/python/data/Python.gitignore index 17ad8c9e..98547b11 100644 --- a/python/rpdk/python/data/Python.gitignore +++ b/python/rpdk/python/data/Python.gitignore @@ -125,3 +125,5 @@ dmypy.json # contains credentials sam-tests/ + +rpdk.log* diff --git a/src/cloudformation_cli_python_lib/interface.py b/src/cloudformation_cli_python_lib/interface.py index 13d8ef6a..248053df 100644 --- a/src/cloudformation_cli_python_lib/interface.py +++ b/src/cloudformation_cli_python_lib/interface.py @@ -130,5 +130,10 @@ class BaseResourceHandlerRequest: clientRequestToken: str desiredResourceState: Optional[BaseModel] previousResourceState: Optional[BaseModel] + desiredResourceTags: Optional[Mapping[str, Any]] + systemTags: Optional[Mapping[str, Any]] + awsAccountId: Optional[str] logicalResourceIdentifier: Optional[str] nextToken: Optional[str] + region: Optional[str] + awsPartition: Optional[str] diff --git a/src/cloudformation_cli_python_lib/metrics.py b/src/cloudformation_cli_python_lib/metrics.py index eed8d27f..d24ad0cf 100644 --- a/src/cloudformation_cli_python_lib/metrics.py +++ b/src/cloudformation_cli_python_lib/metrics.py @@ -49,12 +49,12 @@ def publish_metric( # pylint: disable-msg=too-many-arguments class MetricsPublisherProxy: @staticmethod - def _make_namespace(account_id: str, resource_type: str) -> str: + def _make_namespace(resource_type: str) -> str: suffix = resource_type.replace("::", "/") - return f"{METRIC_NAMESPACE_ROOT}/{account_id}/{suffix}" + return f"{METRIC_NAMESPACE_ROOT}/{suffix}" - def __init__(self, account_id: str, resource_type: str) -> None: - self.namespace = self._make_namespace(account_id, resource_type) + def __init__(self, resource_type: str) -> None: + self.namespace = self._make_namespace(resource_type) self.resource_type = resource_type self._publishers: List[MetricPublisher] = [] diff --git a/src/cloudformation_cli_python_lib/resource.py b/src/cloudformation_cli_python_lib/resource.py index 7f7d09d6..e20c2ed8 100644 --- a/src/cloudformation_cli_python_lib/resource.py +++ b/src/cloudformation_cli_python_lib/resource.py @@ -163,7 +163,11 @@ def _cast_resource_request( clientRequestToken=request.bearerToken, desiredResourceState=request.requestData.resourceProperties, previousResourceState=request.requestData.previousResourceProperties, + desiredResourceTags=request.requestData.stackTags, + systemTags=request.requestData.systemTags, + awsAccountId=request.awsAccountId, logicalResourceIdentifier=request.requestData.logicalResourceId, + region=request.region, ).to_modelled(self._model_cls) except Exception as e: # pylint: disable=broad-except LOG.exception("Invalid request") @@ -191,7 +195,7 @@ def print_or_log(message: str) -> None: request = self._cast_resource_request(event) - metrics = MetricsPublisherProxy(event.awsAccountId, event.resourceType) + metrics = MetricsPublisherProxy(event.resourceType) metrics.add_metrics_publisher(provider_sess) metrics.publish_invocation_metric(datetime.utcnow(), action) diff --git a/src/cloudformation_cli_python_lib/utils.py b/src/cloudformation_cli_python_lib/utils.py index 46b357c0..e4921ff0 100644 --- a/src/cloudformation_cli_python_lib/utils.py +++ b/src/cloudformation_cli_python_lib/utils.py @@ -113,8 +113,12 @@ class UnmodelledRequest: clientRequestToken: str desiredResourceState: Optional[Mapping[str, Any]] = None previousResourceState: Optional[Mapping[str, Any]] = None + desiredResourceTags: Optional[Mapping[str, Any]] = None + systemTags: Optional[Mapping[str, Any]] = None + awsAccountId: Optional[str] = None logicalResourceIdentifier: Optional[str] = None nextToken: Optional[str] = None + region: Optional[str] = None def to_modelled(self, model_cls: Type[BaseModel]) -> BaseResourceHandlerRequest: # pylint: disable=protected-access @@ -122,10 +126,27 @@ def to_modelled(self, model_cls: Type[BaseModel]) -> BaseResourceHandlerRequest: clientRequestToken=self.clientRequestToken, desiredResourceState=model_cls._deserialize(self.desiredResourceState), previousResourceState=model_cls._deserialize(self.previousResourceState), + desiredResourceTags=self.desiredResourceTags, + systemTags=self.systemTags, + awsAccountId=self.awsAccountId, logicalResourceIdentifier=self.logicalResourceIdentifier, nextToken=self.nextToken, + region=self.region, + awsPartition=self.get_partition(self.region), ) + @staticmethod + def get_partition(region: Optional[str]) -> Optional[str]: + if region is None: + return None + + if region.startswith("cn"): + return "aws-cn" + + if region.startswith("us-gov"): + return "aws-gov" + return "aws" + class LambdaContext: get_remaining_time_in_millis: Callable[["LambdaContext"], int] diff --git a/tests/lib/metrics_test.py b/tests/lib/metrics_test.py index 7c03b150..6af1681a 100644 --- a/tests/lib/metrics_test.py +++ b/tests/lib/metrics_test.py @@ -14,10 +14,9 @@ from botocore.stub import Stubber -ACCOUNT_ID = "123412341234" RESOURCE_TYPE = "Aa::Bb::Cc" NAMESPACE = MetricsPublisherProxy._make_namespace( # pylint: disable=protected-access - ACCOUNT_ID, RESOURCE_TYPE + RESOURCE_TYPE ) @@ -74,13 +73,13 @@ def test_put_metric_catches_error(mock_session): def test_publish_exception_metric(mock_session): fake_datetime = datetime(2019, 1, 1) - proxy = MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE) + proxy = MetricsPublisherProxy(RESOURCE_TYPE) proxy.add_metrics_publisher(mock_session) proxy.publish_exception_metric(fake_datetime, Action.CREATE, Exception("fake-err")) expected_calls = [ call.client("cloudwatch"), call.client().put_metric_data( - Namespace="AWS/CloudFormation/123412341234/Aa/Bb/Cc", + Namespace="AWS/CloudFormation/Aa/Bb/Cc", MetricData=[ { "MetricName": MetricTypes.HandlerException.name, @@ -104,14 +103,14 @@ def test_publish_exception_metric(mock_session): def test_publish_invocation_metric(mock_session): fake_datetime = datetime(2019, 1, 1) - proxy = MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE) + proxy = MetricsPublisherProxy(RESOURCE_TYPE) proxy.add_metrics_publisher(mock_session) proxy.publish_invocation_metric(fake_datetime, Action.CREATE) expected_calls = [ call.client("cloudwatch"), call.client().put_metric_data( - Namespace="AWS/CloudFormation/123412341234/Aa/Bb/Cc", + Namespace="AWS/CloudFormation/Aa/Bb/Cc", MetricData=[ { "MetricName": MetricTypes.HandlerInvocationCount.name, @@ -131,14 +130,14 @@ def test_publish_invocation_metric(mock_session): def test_publish_duration_metric(mock_session): fake_datetime = datetime(2019, 1, 1) - proxy = MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE) + proxy = MetricsPublisherProxy(RESOURCE_TYPE) proxy.add_metrics_publisher(mock_session) proxy.publish_duration_metric(fake_datetime, Action.CREATE, 100) expected_calls = [ call.client("cloudwatch"), call.client().put_metric_data( - Namespace="AWS/CloudFormation/123412341234/Aa/Bb/Cc", + Namespace="AWS/CloudFormation/Aa/Bb/Cc", MetricData=[ { "MetricName": MetricTypes.HandlerInvocationDuration.name, @@ -158,14 +157,14 @@ def test_publish_duration_metric(mock_session): def test_publish_log_delivery_exception_metric(mock_session): fake_datetime = datetime(2019, 1, 1) - proxy = MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE) + proxy = MetricsPublisherProxy(RESOURCE_TYPE) proxy.add_metrics_publisher(mock_session) proxy.publish_log_delivery_exception_metric(fake_datetime, TypeError("test")) expected_calls = [ call.client("cloudwatch"), call.client().put_metric_data( - Namespace="AWS/CloudFormation/123412341234/Aa/Bb/Cc", + Namespace="AWS/CloudFormation/Aa/Bb/Cc", MetricData=[ { "MetricName": MetricTypes.HandlerException.name, @@ -191,6 +190,6 @@ def test_publish_log_delivery_exception_metric(mock_session): def test_metrics_publisher_proxy_add_metrics_publisher_none_safe(): - proxy = MetricsPublisherProxy(ACCOUNT_ID, RESOURCE_TYPE) + proxy = MetricsPublisherProxy(RESOURCE_TYPE) proxy.add_metrics_publisher(None) assert proxy._publishers == [] # pylint: disable=protected-access diff --git a/tests/lib/utils_test.py b/tests/lib/utils_test.py index e2ce4a6f..62e3eefa 100644 --- a/tests/lib/utils_test.py +++ b/tests/lib/utils_test.py @@ -66,10 +66,6 @@ def test_handler_request_serde_roundtrip(): "requestContext": { "invocation": 2, "callbackContext": {"contextPropertyA": "Value"}, - "cloudWatchEventsRuleName": "reinvoke-handler-4754ac8a-623b-45fe-84bc-f5394" - "118a8be", - "cloudWatchEventsTargetId": "reinvoke-target-4754ac8a-623b-45fe-84bc-f53941" - "18a8be", }, "requestData": { "callerCredentials": None, @@ -102,7 +98,8 @@ def test_handler_request_serde_roundtrip(): assert ser == expected -def test_unmodelled_request_to_modelled(): +@pytest.mark.parametrize("region", ("us-east-1", "cn-region1", "us-gov-region1")) +def test_unmodelled_request_to_modelled(region): model_cls = Mock(spec_set=BaseModel) model_cls._deserialize.side_effect = [sentinel.new, sentinel.old] @@ -112,6 +109,7 @@ def test_unmodelled_request_to_modelled(): previousResourceState={"state": "old"}, logicalResourceIdentifier="bar", nextToken="baz", + region=region, ) modelled = unmodelled.to_modelled(model_cls)