Repository: cloud-custodian/cloud-custodian

Issue:
trunk - empty policy files cause exception
**Describe the bug**
due to lazy/on demand schema creation, empty policies cause a schema validation error on trunk

**To Reproduce**
run a policy file with an empty set of policies

**Expected behavior**
a run proceeds

**Additional context**
normalizing on policy loader might be helpful, schema subcommand could also be optimized.




Discussion:

Resolving patch:
diff --git a/c7n/schema.py b/c7n/schema.py
--- a/c7n/schema.py
+++ b/c7n/schema.py
@@ -331,6 +331,9 @@ def generate(resource_types=()):
         }
     }
 
+    # allow empty policies with lazy load
+    if not resource_refs:
+        schema['properties']['policies']['items'] = {'type': 'object'}
     return schema
 
 


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -19,11 +19,13 @@
 
 from c7n.exceptions import PolicyValidationError
 from c7n.filters import ValueFilter
+from c7n.registry import PluginRegistry
 from c7n.resources import load_resources
 from c7n.schema import (
     StructureParser, ElementSchema, resource_vocabulary,
     JsonSchemaValidator, validate, generate,
     specific_error, policy_error_scope)
+from c7n import schema
 from .common import BaseTest
 
 
@@ -140,6 +142,14 @@ def test_empty_skeleton(self):
                 {"policies": []}),
             [])
 
+    def test_empty_with_lazy_load(self):
+        empty_registry = PluginRegistry('stuff')
+        self.patch(schema, 'clouds', empty_registry)
+        policy_schema = generate()
+        self.assertEqual(
+            policy_schema['properties']['policies']['items'],
+            {'type': 'object'})
+
     def test_duplicate_policies(self):
         data = {
             "policies": [
@@ -275,7 +285,7 @@ def test_handle_specific_error_fail(self, mock_specific_error):
         mock_specific_error.side_effect = ValueError(
             "The specific error crapped out hard"
         )
-
+        load_resources(('aws.ec2',))
         resp = validate(data)
         # if it is 2, then we know we got the exception from specific_error
         self.assertEqual(len(resp), 2)
@@ -293,6 +303,7 @@ def test_semantic_error_with_nested_resource_key(self):
                      'skip_missing': True,
                      'key': 'VolumeId',
                      'tags': 'Team'}]}]}
+        load_resources(('aws.ebs',))
         validator = self.get_validator(data)
         errors = list(validator.iter_errors(data))
         self.assertEqual(len(errors), 1)
@@ -304,6 +315,7 @@ def test_vars_and_tags(self):
             "vars": {"alpha": 1, "beta": 2},
             "policies": [{"name": "test", "resource": "ec2", "tags": ["controls"]}],
         }
+        load_resources(('aws.ec2',))
         validator = self.get_validator(data)
         self.assertEqual(list(validator.iter_errors(data)), [])
 
@@ -374,14 +386,23 @@ def test_nested_bool_operators(self):
                 }
             ]
         }
-        errors = list(self.validator.iter_errors(data))
+
+        load_resources(('aws.ec2',))
+        validator = self.policy_loader.validator.gen_schema(('aws.ec2',))
+        errors = list(validator.iter_errors(data))
         self.assertEqual(errors, [])
 
     def test_bool_operator_child_validation(self):
         data = {'policies': [
-            {'name': 'test', 'resource': 'ec2', 'filters': [
-                {'or': [{'type': 'imagex', 'key': 'tag:Foo', 'value': 'a'}]}]}]}
-        errors = list(self.validator.iter_errors(data))
+            {'name': 'test',
+             'resource': 'ec2',
+             'filters': [
+                 {'or': [
+                     {'type': 'imagex', 'key': 'tag:Foo', 'value': 'a'}
+                 ]}]}]}
+        load_resources(('aws.ec2',))
+        validator = self.policy_loader.validator.gen_schema(('aws.ec2',))
+        errors = list(validator.iter_errors(data))
         self.assertTrue(errors)
 
     def test_value_filter_short_form(self):