From 61b46bf6b35da73f68cb0d3c0fbe90f2810dc7f4 Mon Sep 17 00:00:00 2001 From: Anton Mokhovikov Date: Mon, 11 Mar 2024 10:11:26 -0700 Subject: [PATCH] Added special handling for Leaf Constructs (#47) --- src/rpdk/guard_rail/core/stateful.py | 45 ++++++ tests/unit/core/test_stateful.py | 227 ++++++++++++++++++++++++++- 2 files changed, 265 insertions(+), 7 deletions(-) diff --git a/src/rpdk/guard_rail/core/stateful.py b/src/rpdk/guard_rail/core/stateful.py index b1aa4da..e904251 100644 --- a/src/rpdk/guard_rail/core/stateful.py +++ b/src/rpdk/guard_rail/core/stateful.py @@ -63,6 +63,12 @@ class DIFFKEYS: "oneOf", } +cfn_leaf_level_constructs = { + "relationshipRef", + "insertionOrder", + "arrayType", +} + native_constructs = { "type", "description", @@ -78,6 +84,7 @@ class DIFFKEYS: "contains", "items", "additionalProperties", + "uniqueItems", } @@ -112,6 +119,7 @@ def _is_resource_property(path_list): len(path_list) > 0 and path_list[0] == PROPERTIES and path_list[-1] not in native_constructs + and path_list[-1] not in cfn_leaf_level_constructs ) @@ -125,6 +133,11 @@ def _is_json_construct(path_list): return len(path_list) > 0 and path_list[-1] in native_constructs +def _is_cfn_leaf_construct(path_list): + """This method defines json constructs""" + return len(path_list) > 0 and path_list[-1] in cfn_leaf_level_constructs + + def _get_path(path_list): """This method converts array into schema path notation""" return "/".join([""] + path_list) @@ -166,6 +179,13 @@ def _traverse_nested_properties( diff_value (Any): arbitrary value """ + # cfn might have more custom leaf level props + # if not specifically added to cfn_leaf_level_constructs + # it could be traversed further and interpreted as a property + # this hedges out from lookup/attribute/unbounded exceptions + if not diff_value or not isinstance(diff_value, dict): + return + # if type is absent, then we are dealing with properties if "type" not in diff_value: for property_name, property_definition in diff_value.items(): @@ -236,6 +256,13 @@ def __translate_iter_added_diff(diffkey, schema_meta_diff, diff_value): diffkey, value, ) + if _is_cfn_leaf_construct(path_list): + _add_item( + schema_meta_diff, + path_list[-1], + diffkey, + value, + ) # using partial to avoid code repetition append_added = partial(__translate_iter_added_diff, DIFFKEYS.ADDED) @@ -285,6 +312,13 @@ def __translate_dict_diff(diffkey, schema_meta_diff, diff_value): diffkey, _get_path(path_list[:-1]), ) + if _is_cfn_leaf_construct(path_list): + _add_item( + schema_meta_diff, + path_list[-1], + diffkey, + _get_path(path_list[:-1]), + ) # using partial to avoid code repetition append_added = partial(__translate_dict_diff, DIFFKEYS.ADDED) @@ -336,6 +370,17 @@ def _translate_values_changed_diff_(schema_meta_diff, diff_value): DIFFKEYS.NEW_VALUE: value[DIFFKEYS.NEW_VALUE], }, ) + if _is_cfn_leaf_construct(path_list): + _add_item( + schema_meta_diff, + path_list[-1], + DIFFKEYS.CHANGED, + { + DIFFKEYS.PROPERTY: _get_path(path_list[:-1]), + DIFFKEYS.OLD_VALUE: value[DIFFKEYS.OLD_VALUE], + DIFFKEYS.NEW_VALUE: value[DIFFKEYS.NEW_VALUE], + }, + ) def _translate_meta_diff(iterable: Iterable): diff --git a/tests/unit/core/test_stateful.py b/tests/unit/core/test_stateful.py index 28e6a5f..585ccf3 100644 --- a/tests/unit/core/test_stateful.py +++ b/tests/unit/core/test_stateful.py @@ -1,3 +1,4 @@ +# pylint: disable=C0301 """ Unit test for stateful.py """ @@ -119,6 +120,7 @@ "Name": {"type": "string"}, "Configurations": { # recursive property "type": "array", + "insertionOrder": True, "items": {}, # ends at this level }, }, @@ -166,6 +168,7 @@ "properties": { "Tags": { "type": "array", + "insertionOrder": True, "items": { "type": "object", "properties": { @@ -181,31 +184,63 @@ "added": [ "/properties/Tags/*/Value", ] - } + }, + "insertionOrder": {"added": ["/properties/Tags"]}, }, { "properties": { "removed": [ "/properties/Tags/*/Value", ] - } + }, + "insertionOrder": {"removed": ["/properties/Tags"]}, }, ), # Test Case #6: Modified Nested Property ( { "properties": { + "Configuration": { + "type": "object", + "properties": { + "ExecuteCommandConfiguration": { + "type": "object", + "properties": { + "KmsKeyId": { + "type": "string", + "relationshipRef": { + "typeName": "AWS::KMS::Key", + "propertyPath": "/properties/Arn", + }, + } + }, + } + }, + }, "Tags": { "type": "array", "items": { "type": "object", "properties": {"Key": {"type": "string"}}, }, - } + }, } }, { "properties": { + "Configuration": { + "type": "object", + "properties": { + "ExecuteCommandConfiguration": { + "type": "object", + "properties": { + "KmsKeyId": { + "type": "string", + } + }, + } + }, + }, "Tags": { "type": "array", "items": { @@ -214,10 +249,15 @@ "Key": {"type": "int"}, }, }, - } + }, } }, { + "relationshipRef": { + "removed": [ + "/properties/Configuration/properties/ExecuteCommandConfiguration/properties/KmsKeyId" + ] + }, "type": { "changed": [ { @@ -226,9 +266,14 @@ "property": "/properties/Tags/*/Key", } ] - } + }, }, { + "relationshipRef": { + "added": [ + "/properties/Configuration/properties/ExecuteCommandConfiguration/properties/KmsKeyId" + ] + }, "type": { "changed": [ { @@ -237,7 +282,7 @@ "property": "/properties/Tags/*/Key", } ] - } + }, }, ), # Test Case #7: New Two Level Nested Property @@ -355,7 +400,175 @@ } }, ), - # # Test Case #8: New Property with nested combiner (We might shelf this for now) + # Test Case #8: ECS Schema Snippet with cfn leaf constructs + # almost integ tests but not because we are not verifying checks + ( + { + "definitions": { + "CapacityProviderStrategyItem": { + "description": "A capacity provider strategy consists of one or more capacity providers along with the `base` and `weight` to assign to them. A capacity provider must be associated with the cluster to be used in a capacity provider strategy. The PutClusterCapacityProviders API is used to associate a capacity provider with a cluster. Only capacity providers with an `ACTIVE` or `UPDATING` status can be used.", + "additionalProperties": False, + "type": "object", + "properties": { + "CapacityProvider": {"type": "string"}, + "Weight": {"type": "integer"}, + "Base": {"type": "integer"}, + }, + }, + "Configuration": { + "type": "object", + "properties": { + "ExecuteCommandConfiguration": { + "$ref": "#/definitions/ExecuteCommandConfiguration" + } + }, + "additionalProperties": False, + }, + "ExecuteCommandConfiguration": { + "type": "object", + "properties": {"KmsKeyId": {"type": "string"}}, + "additionalProperties": False, + }, + "ClusterSettings": { + "description": "The settings to use when creating a cluster. This parameter is used to turn on CloudWatch Container Insights for a cluster.", + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "The name of the cluster setting. The value is ``containerInsights`` .", + }, + "Value": { + "type": "string", + "description": "The value to set for the cluster setting. The supported values are ``enabled`` and ``disabled``. \n If you set ``name`` to ``containerInsights`` and ``value`` to ``enabled``, CloudWatch Container Insights will be on for the cluster, otherwise it will be off unless the ``containerInsights`` account setting is turned on. If a cluster value is specified, it will override the ``containerInsights`` value set with [PutAccountSetting](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_PutAccountSetting.html) or [PutAccountSettingDefault](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_PutAccountSettingDefault.html).", + }, + }, + "additionalProperties": False, + }, + }, + "properties": { + "ClusterSettings": { + "type": "array", + "insertionOrder": True, + "items": {"$ref": "#/definitions/ClusterSettings"}, + "description": "The settings to use when creating a cluster. This parameter is used to turn on CloudWatch Container Insights for a cluster.", + }, + "DefaultCapacityProviderStrategy": { + "type": "array", + "items": {"$ref": "#/definitions/CapacityProviderStrategyItem"}, + "description": "The default capacity provider strategy for the cluster. When services or tasks are run in the cluster with no launch type or capacity provider strategy specified, the default capacity provider strategy is used.", + }, + "Configuration": {"$ref": "#/definitions/Configuration"}, + "Arn": {"type": "string"}, + }, + }, + { + "definitions": { + "CapacityProviderStrategyItem": { + "description": "A capacity provider strategy consists of one or more capacity providers along with the `base` and `weight` to assign to them. A capacity provider must be associated with the cluster to be used in a capacity provider strategy. The PutClusterCapacityProviders API is used to associate a capacity provider with a cluster. Only capacity providers with an `ACTIVE` or `UPDATING` status can be used.", + "additionalProperties": False, + "type": "object", + "properties": { + "CapacityProvider": { + "type": "string", + "relationshipRef": { + "typeName": "AWS::ECS::CapacityProvider", + "propertyPath": "/properties/Name", + }, + }, + "Weight": {"type": "integer"}, + "Base": {"type": "integer"}, + }, + }, + "Configuration": { + "type": "object", + "properties": { + "ExecuteCommandConfiguration": { + "$ref": "#/definitions/ExecuteCommandConfiguration" + } + }, + "additionalProperties": False, + }, + "ExecuteCommandConfiguration": { + "type": "object", + "properties": { + "KmsKeyId": { + "type": "string", + "relationshipRef": { + "typeName": "AWS::KMS::Key", + "propertyPath": "/properties/Arn", + }, + }, + "Logging": { + "type": "string", + "description": "The log setting to use for redirecting logs for your execute command results. The following log settings are available.\n + ``NONE``: The execute command session is not logged.\n + ``DEFAULT``: The ``awslogs`` configuration in the task definition is used. If no logging parameter is specified, it defaults to this value. If no ``awslogs`` log driver is configured in the task definition, the output won't be logged.\n + ``OVERRIDE``: Specify the logging details as a part of ``logConfiguration``. If the ``OVERRIDE`` logging option is specified, the ``logConfiguration`` is required.", + }, + }, + "additionalProperties": False, + }, + "ClusterSettings": { + "description": "The settings to use when creating a cluster. This parameter is used to turn on CloudWatch Container Insights for a cluster.", + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "The name of the cluster setting. The value is ``containerInsights`` .", + }, + "Value": { + "type": "string", + "description": "The value to set for the cluster setting. The supported values are ``enabled`` and ``disabled``. \n If you set ``name`` to ``containerInsights`` and ``value`` to ``enabled``, CloudWatch Container Insights will be on for the cluster, otherwise it will be off unless the ``containerInsights`` account setting is turned on. If a cluster value is specified, it will override the ``containerInsights`` value set with [PutAccountSetting](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_PutAccountSetting.html) or [PutAccountSettingDefault](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_PutAccountSettingDefault.html).", + }, + }, + "additionalProperties": False, + }, + }, + "properties": { + "ClusterSettings": { + "type": "array", + "items": {"$ref": "#/definitions/ClusterSettings"}, + "description": "The settings to use when creating a cluster. This parameter is used to turn on CloudWatch Container Insights for a cluster.", + }, + "DefaultCapacityProviderStrategy": { + "type": "array", + "items": {"$ref": "#/definitions/CapacityProviderStrategyItem"}, + "description": "The default capacity provider strategy for the cluster. When services or tasks are run in the cluster with no launch type or capacity provider strategy specified, the default capacity provider strategy is used.", + }, + "ECSEndpoint": {"type": "string"}, + "Configuration": {"$ref": "#/definitions/Configuration"}, + "Arn": {"type": "string"}, + }, + }, + { + "properties": { + "added": [ + "/properties/ECSEndpoint", + "/properties/Configuration/properties/ExecuteCommandConfiguration/properties/Logging", + ] + }, + "relationshipRef": { + "added": [ + "/properties/DefaultCapacityProviderStrategy/*/CapacityProvider", + "/properties/Configuration/properties/ExecuteCommandConfiguration/properties/KmsKeyId", + ] + }, + "insertionOrder": {"removed": ["/properties/ClusterSettings"]}, + }, + { + "properties": { + "removed": [ + "/properties/ECSEndpoint", + "/properties/Configuration/properties/ExecuteCommandConfiguration/properties/Logging", + ] + }, + "relationshipRef": { + "removed": [ + "/properties/DefaultCapacityProviderStrategy/*/CapacityProvider", + "/properties/Configuration/properties/ExecuteCommandConfiguration/properties/KmsKeyId", + ] + }, + "insertionOrder": {"added": ["/properties/ClusterSettings"]}, + }, + ), + # # Test Case #9: New Property with nested combiner (We might shelf this for now) # ( # { # "properties": {