Repository: cloud-custodian/cloud-custodian

Issue:
Policy validation should raise an error if multiple "policies" statement usage
In case of multiple "policies:" statements in a policy file, c7n only deploys those under the last "policies:" and not the one in previous without raising a warning or an error. 
I think this should raise an error in validation phase.
For example:
```
policies:
    - name: some name
    ....
policies:
    - name: another name
   ....
```
Only the policy named "another name" will be deployed/run


Discussion:
That sounds useful, effectively detect and warn/raise on duplicate keys in a mapping.
One other consideration here it may very well lead to flagging some policy files as incompatible/errors. ie. this is likely a place where we should have differential behavior for run and validate (both of which run jsonschema validation). For run its likely that this would lead to compatiblities issues in unattended systems. For validate which is typically run in ci systems for policies this seems okay.
IMHO, if you put multiple "policies:", it should be an error on validate and run. Because, if validate turns to say everything fine, at run, it will run/deploy half of the policies

Resolving patch:
diff --git a/c7n/commands.py b/c7n/commands.py
--- a/c7n/commands.py
+++ b/c7n/commands.py
@@ -17,7 +17,6 @@
 from datetime import timedelta, datetime
 from functools import wraps
 import inspect
-import json
 import logging
 import os
 import pprint
@@ -26,11 +25,12 @@
 
 import six
 import yaml
+from yaml.constructor import ConstructorError
 
 from c7n.exceptions import ClientError
 from c7n.provider import clouds
 from c7n.policy import Policy, PolicyCollection, load as policy_load
-from c7n.utils import dumps, load_file, local_session
+from c7n.utils import dumps, load_file, local_session, SafeLoader
 from c7n.config import Bag, Config
 from c7n import provider
 from c7n.resources import load_resources
@@ -171,6 +171,27 @@ def _print_no_policies_warning(options, policies):
         log.warning('Empty policy file(s).  Nothing to do.')
 
 
+class DuplicateKeyCheckLoader(SafeLoader):
+
+    def construct_mapping(self, node, deep=False):
+        if not isinstance(node, yaml.MappingNode):
+            raise ConstructorError(None, None,
+                    "expected a mapping node, but found %s" % node.id,
+                    node.start_mark)
+        key_set = set()
+        for key_node, value_node in node.value:
+            if not isinstance(key_node, yaml.ScalarNode):
+                continue
+            k = key_node.value
+            if k in key_set:
+                raise ConstructorError(
+                    "while constructing a mapping", node.start_mark,
+                    "found duplicate key", key_node.start_mark)
+            key_set.add(k)
+
+        return super(DuplicateKeyCheckLoader, self).construct_mapping(node, deep)
+
+
 def validate(options):
     from c7n import schema
     load_resources()
@@ -189,11 +210,10 @@ def validate(options):
 
         options.dryrun = True
         fmt = config_file.rsplit('.', 1)[-1]
+
         with open(config_file) as fh:
-            if fmt in ('yml', 'yaml'):
-                data = yaml.safe_load(fh.read())
-            elif fmt in ('json',):
-                data = json.load(fh)
+            if fmt in ('yml', 'yaml', 'json'):
+                data = yaml.load(fh.read(), Loader=DuplicateKeyCheckLoader)
             else:
                 log.error("The config file must end in .json, .yml or .yaml.")
                 raise ValueError("The config file must end in .json, .yml or .yaml.")


Test patch:
diff --git a/tests/data/test_policies/dup-policies.yml b/tests/data/test_policies/dup-policies.yml
new file mode 100644
--- /dev/null
+++ b/tests/data/test_policies/dup-policies.yml
@@ -0,0 +1,4 @@
+policies: []
+policies:
+  - name: ec2
+    resource: aws.ec2
diff --git a/tests/data/test_policies/dup-policy-keys.yml b/tests/data/test_policies/dup-policy-keys.yml
new file mode 100644
--- /dev/null
+++ b/tests/data/test_policies/dup-policy-keys.yml
@@ -0,0 +1,4 @@
+policies:
+  - name: scooter
+    resource: aws.ec2
+    resource: ec2
diff --git a/tests/test_validation.py b/tests/test_validation.py
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -14,6 +14,7 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import argparse
+import yaml
 
 from .common import BaseTest
 from c7n.commands import validate as validate_yaml_policies
@@ -21,6 +22,35 @@
 
 class CommandsValidateTest(BaseTest):
 
+    def test_duplicate_key(self):
+        # try dupes with top level keys
+        yaml_validate_options = argparse.Namespace(
+            command="c7n.commands.validate",
+            config=None,
+            configs=[
+                "tests/data/test_policies/dup-policies.yml"],
+            debug=False,
+            subparser="validate",
+            verbose=False)
+
+        with self.assertRaises(yaml.YAMLError) as err:
+            validate_yaml_policies(yaml_validate_options)
+        self.assertTrue('found duplicate key' in str(err.exception))
+
+        # try dupes with policy attributes
+        yaml_validate_options = argparse.Namespace(
+            command="c7n.commands.validate",
+            config=None,
+            configs=[
+                "tests/data/test_policies/dup-policy-keys.yml"],
+            debug=False,
+            subparser="validate",
+            verbose=False)
+
+        with self.assertRaises(yaml.YAMLError) as err:
+            validate_yaml_policies(yaml_validate_options)
+        self.assertTrue('found duplicate key' in str(err.exception))
+
     def test_failed_validation(self):
         yaml_validate_options = argparse.Namespace(
             command="c7n.commands.validate",