Repository: aws-cloudformation/cfn-lint

Issue:
Add a check that CloudWatch Events Rules have <= 5 targets
*cfn-lint version: (`cfn-lint --version`) 0.5.0* (but nothing changed since afaict)

*Description of issue.*

CloudWatch Events has a limit of 10 targets per Rule, but this isn't validated by `ValidateTemplate` or `cfn-lint`'s rules. Consequently one has to wait til deploy time to discover this error message:

> CREATE_FAILED: At most 10 targets allowed.

Failing template:

```yaml
Resources:
  MyCronRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: cron(0 6 * * ? *)
      Targets:
      - Id: Job1
        Arn: arn:target
      - Id: Job2
        Arn: arn:target
      - Id: Job3
        Arn: arn:target
      - Id: Job4
        Arn: arn:target
      - Id: Job5
        Arn: arn:target
      - Id: Job6
        Arn: arn:target
      - Id: Job7
        Arn: arn:target
      - Id: Job8
        Arn: arn:target
      - Id: Job9
        Arn: arn:target
      - Id: Job10
        Arn: arn:target
      - Id: Job11
        Arn: arn:target
```


Discussion:
Thanks.  These are the best type of rules for us to add.  We'll get this added.
I'll also take a look at a PR myself if I find the time
According to https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/cloudwatch_limits_cwe.html the limit is actually 5 targets per rule. At 6 targets I get a different error message to at 11 targets:

```
$ aws events put-targets --rule test-limits --targets '[
    {"Id": "Test1", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test2", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test3", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test4", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test5", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test6", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"}
]'

An error occurred (LimitExceededException) when calling the PutTargets operation: The requested resource exceeds the maximum number allowed.
$ aws events put-targets --rule test-limits --targets '[
    {"Id": "Test1", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test2", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test3", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test4", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test5", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test6", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test7", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test8", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test9", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test10", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"},
    {"Id": "Test11", "Arn": "arn:aws:lambda:us-east-1:123456789012:function:example"}
]'

An error occurred (ValidationException) when calling the PutTargets operation: At most 10 targets allowed.
```

This might imply the ability to use 6-10 targets is possible with a limit increase, although such a limit increase isn't documented. I've reported to support.
Support confirmed there's no limit increase possible, just a weirdness of the API that it returns two different error messages at different numbers of targets. So a hardcoded limit of 5 would be fine in `cfn-lint`.
Thanks for checking that should make things easier.  It would have been an odd rule if it was a soft limit. 

Resolving patch:
diff --git a/src/cfnlint/rules/resources/events/RuleTargetsLimit.py b/src/cfnlint/rules/resources/events/RuleTargetsLimit.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/events/RuleTargetsLimit.py
@@ -0,0 +1,67 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+from cfnlint import CloudFormationLintRule
+from cfnlint import RuleMatch
+
+
+class RuleTargetsLimit(CloudFormationLintRule):
+    """Check State Machine Definition"""
+    id = 'E3021'
+    shortdesc = 'Check Events Rule Targets are less than or equal to 5'
+    description = 'CloudWatch Events Rule can only support up to 5 targets'
+    source_url = 'https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/cloudwatch_limits_cwe.html'
+    tags = ['resources', 'events']
+    max_count = 5
+
+    def __init__(self):
+        """Init"""
+        self.resource_property_types.append('AWS::Events::Rule')
+        self.limits = {}
+
+    # pylint: disable=W0613
+    def check_value(self, value, path):
+        """Count them up """
+        if path[4] == 'Fn::If':
+            resource_name = '%s.%s' % (path[1], path[5])
+        else:
+            resource_name = path[1]
+        if resource_name not in self.limits:
+            self.limits[resource_name] = {
+                'count': 0,
+                'path': path[:-1]
+            }
+
+        self.limits[resource_name]['count'] += 1
+        return []
+
+    def match_resource_properties(self, properties, _, path, cfn):
+        """Check CloudFormation Properties"""
+        matches = []
+
+        matches.extend(
+            cfn.check_value(
+                obj=properties, key='Targets',
+                path=path[:],
+                check_value=self.check_value
+            ))
+
+        for _, limit in self.limits.items():
+            if limit['count'] > self.max_count:
+                message = 'An Events Rule can have up to {0} Targets'
+                matches.append(RuleMatch(limit['path'], message.format(self.max_count)))
+
+        return matches
diff --git a/src/cfnlint/rules/resources/events/__init__.py b/src/cfnlint/rules/resources/events/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/events/__init__.py
@@ -0,0 +1,16 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""


Test patch:
diff --git a/test/fixtures/templates/bad/resources/events/rule_targets_limit.yaml b/test/fixtures/templates/bad/resources/events/rule_targets_limit.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/events/rule_targets_limit.yaml
@@ -0,0 +1,42 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Conditions:
+  condition: !Equals [!Ref 'AWS::Region', 'us-east-1']
+Resources:
+  MyCronRule:
+    Type: AWS::Events::Rule
+    Properties:
+      ScheduleExpression: cron(0 6 * * ? *)
+      Targets:
+        Fn::If:
+        - condition
+        - - Id: Job1
+            Arn: arn:target
+          - Id: Job2
+            Arn: arn:target
+          - Id: Job3
+            Arn: arn:target
+          - Id: Job4
+            Arn: arn:target
+          - Id: Job5
+            Arn: arn:target
+          - Id: Job6
+            Arn: arn:target
+          - Id: Job7
+            Arn: arn:target
+          - Id: Job8
+            Arn: arn:target
+          - Id: Job9
+            Arn: arn:target
+          - Id: Job10
+            Arn: arn:target
+          - Id: Job11
+            Arn: arn:target
+        - - Id: Job1
+            Arn: arn:target
+          - Id: Job2
+            Arn: arn:target
+          - Id: Job3
+            Arn: arn:target
+          - Id: Job4
+            Arn: arn:target
diff --git a/test/fixtures/templates/good/resources/events/rule_targets_limit.yaml b/test/fixtures/templates/good/resources/events/rule_targets_limit.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/events/rule_targets_limit.yaml
@@ -0,0 +1,30 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Conditions:
+  condition: !Equals [!Ref 'AWS::Region', 'us-east-1']
+Resources:
+  MyCronRule:
+    Type: AWS::Events::Rule
+    Properties:
+      ScheduleExpression: cron(0 6 * * ? *)
+      Targets:
+        Fn::If:
+        - condition
+        - - Id: Job1
+            Arn: arn:target
+          - Id: Job2
+            Arn: arn:target
+          - Id: Job3
+            Arn: arn:target
+          - Id: Job4
+            Arn: arn:target
+          - Id: Job5
+            Arn: arn:target
+        - - Id: Job1
+            Arn: arn:target
+          - Id: Job2
+            Arn: arn:target
+          - Id: Job3
+            Arn: arn:target
+          - Id: Job4
+            Arn: arn:target
diff --git a/test/rules/resources/events/__init__.py b/test/rules/resources/events/__init__.py
new file mode 100644
--- /dev/null
+++ b/test/rules/resources/events/__init__.py
@@ -0,0 +1,16 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
diff --git a/test/rules/resources/events/test_rule_targets_limit.py b/test/rules/resources/events/test_rule_targets_limit.py
new file mode 100644
--- /dev/null
+++ b/test/rules/resources/events/test_rule_targets_limit.py
@@ -0,0 +1,37 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+from cfnlint.rules.resources.events.RuleTargetsLimit import RuleTargetsLimit  # pylint: disable=E0401
+from ... import BaseRuleTestCase
+
+
+class TestRuleTargetsLimit(BaseRuleTestCase):
+    """Test Limits of Event Rules Targets"""
+    def setUp(self):
+        """Setup"""
+        super(TestRuleTargetsLimit, self).setUp()
+        self.collection.register(RuleTargetsLimit())
+        self.success_templates = [
+            'fixtures/templates/good/resources/events/rule_targets_limit.yaml'
+        ]
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative_alias(self):
+        """Test failure"""
+        self.helper_file_negative('fixtures/templates/bad/resources/events/rule_targets_limit.yaml', 1)