diff --git a/src/cfnlint/rules/resources/cloudformation/ServiceTimeout.py b/src/cfnlint/rules/resources/cloudformation/ServiceTimeout.py new file mode 100644 index 0000000000..a4a1f29823 --- /dev/null +++ b/src/cfnlint/rules/resources/cloudformation/ServiceTimeout.py @@ -0,0 +1,56 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from cfnlint.rules import CloudFormationLintRule, RuleMatch + + +class ServiceTimeout(CloudFormationLintRule): + """Check a ServiceTimeout property is specified for custom resources""" + + id = "W3046" + shortdesc = "Ensure a service timeout is specified" + description = """ + Custom resources should have a ServiceTimeout property specified. + The default service timeout is 60 minutes. + Specify a short timeout to reduce waiting if the custom resource fails + """ + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-customresource.html#cfn-cloudformation-customresource-servicetimeout" + tags = [ + "resources", + "cloudformation", + "custom resource", + ] + + def match(self, cfn): + """Basic Rule Matching""" + + matches = [] + + resources = self._get_custom_resources(cfn) + + for resource_name, attributes in resources.items(): + properties = attributes.get("Properties", {}) + resource_type = attributes.get("Type", None) + + if "ServiceTimeout" not in properties: + message = f"Missing ServiceTimeout property in {resource_type}" + matches.append(RuleMatch(["Resources", resource_name], message)) + + return matches + + def _get_custom_resources(self, cfn): + resources = cfn.template.get("Resources", {}) + if not isinstance(resources, dict): + return {} + + results = {} + for k, v in resources.items(): + if isinstance(v, dict): + if (v.get("Type", None) == "AWS::CloudFormation::CustomResource") or ( + v.get("Type", None).startswith("Custom::") + ): + results[k] = v + + return results diff --git a/test/fixtures/templates/bad/resources/cloudformation/service_timeout.yaml b/test/fixtures/templates/bad/resources/cloudformation/service_timeout.yaml new file mode 100644 index 0000000000..5de3f10253 --- /dev/null +++ b/test/fixtures/templates/bad/resources/cloudformation/service_timeout.yaml @@ -0,0 +1,10 @@ +Resources: + CustomResource: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: "arn::aws::fake" + + CustomResource2: + Type: Custom::CustomResource + Properties: + ServiceToken: "arn::aws::fake" diff --git a/test/fixtures/templates/good/resources/cloudformation/service_timeout.yaml b/test/fixtures/templates/good/resources/cloudformation/service_timeout.yaml new file mode 100644 index 0000000000..3abafd66c7 --- /dev/null +++ b/test/fixtures/templates/good/resources/cloudformation/service_timeout.yaml @@ -0,0 +1,12 @@ +Resources: + CustomResource: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: "arn::aws::fake" + ServiceTimeout: 60 + + CustomResource2: + Type: Custom::CustomResource + Properties: + ServiceToken: "arn::aws::fake" + ServiceTimeout: 90 diff --git a/test/unit/rules/resources/cloudformation/test_service_timeout.py b/test/unit/rules/resources/cloudformation/test_service_timeout.py new file mode 100644 index 0000000000..330fec4eb6 --- /dev/null +++ b/test/unit/rules/resources/cloudformation/test_service_timeout.py @@ -0,0 +1,40 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +import logging +from test.unit.rules import BaseRuleTestCase + +from cfnlint.rules.resources.cloudformation.ServiceTimeout import ServiceTimeout + + +class TestServiceTimeout(BaseRuleTestCase): + """Test CloudFormation Nested stack parameters""" + + def tearDown(self) -> None: + super().tearDown() + logger = logging.getLogger("cfnlint.decode.decode") + logger.disabled = False + + def setUp(self): + """Setup""" + super(ServiceTimeout, self).setUp() + self.collection.register(ServiceTimeout()) + logger = logging.getLogger("cfnlint.decode.decode") + logger.disabled = True + self.success_templates = [ + "test/fixtures/templates/good/resources/cloudformation/service_timeout.yaml" + ] + + def test_file_positive(self): + """Test Positive""" + self.helper_file_positive() + + def test_file_negative(self): + """Test failure""" + err_count = 2 + self.helper_file_negative( + "test/fixtures/templates/bad/resources/cloudformation/service_timeout.yaml", + err_count, + )