Repository: cloud-custodian/cloud-custodian

Issue:
Schema error with `aws.network-addr`
**Describe the bug**
Schema error with `aws.network-addr`. 

**To Reproduce**
Attempt to get the schema, notice `aws.network-addr` is in the list. Next, try to get the schema for `aws.network-addr`, notice that custodian throws an error.

```bash
# custodian schema
resources:
... snipped...
- aws.network-acl
- aws.network-addr
- aws.ops-item
...
# custodian schema aws.network-addr
2020-05-14 13:50:11,216: custodian.commands:ERROR aws.network-addr is not a valid resource
```

**Expected behavior**
I would expect the schema to load.

**Background (please complete the following information):**
 - OS: ubuntu 18.0.4
 - Python Version: 3.7.5
 - Custodian Version: 0.9.1
 - Tool Version: [if applicable]
 - Cloud Provider: aws
 - Traceback: [if applicable, please exclude sensitive/account information]
 - `custodian version --debug` output:

```
# custodian version
0.9.1
(venv) root@ip-10-66-150-45:/opt/stickler# custodian version --debug

Please copy/paste the following info along with any bug reports:

Custodian:   0.9.1
Python:      3.7.5 (default, Nov  7 2019, 10:50:52)
             [GCC 8.3.0]
Platform:    posix.uname_result(sysname='Linux', nodename='ip-10-66-150-45', release='5.3.0-1017-aws', version='#18~18.04.1-Ubuntu SMP Wed Apr 8 15:12:16 UTC 2020', machine='x86_64')
Using venv:  True
Docker: False
Installed:

argcomplete==1.11.1
attrs==19.3.0
boto3==1.12.47
botocore==1.15.47
docutils==0.15.2
importlib-metadata==1.6.0
jmespath==0.9.5
jsonschema==3.2.0
pyrsistent==0.16.0
python-dateutil==2.8.1
pyyaml==5.3.1
s3transfer==0.3.3
setuptools==46.2.0
six==1.14.0
tabulate==0.8.7
urllib3==1.25.9
zipp==3.1.0
```

**Additional context**
Add any other context about the problem here.


Discussion:

Patch:
diff --git a/c7n/commands.py b/c7n/commands.py
index e0e41a7b2..2a29bf925 100644
--- a/c7n/commands.py
+++ b/c7n/commands.py
@@ -379,14 +379,14 @@ def schema_cmd(options):
         components[0] = '%s.%s' % (cloud_provider, components[0])
         load_resources((components[0],))
         resource_mapping = schema.resource_vocabulary(
-            cloud_provider)
+            cloud_provider, aliases=True)
     elif components[0] == 'mode':
         load_available(resources=False)
         resource_mapping = schema.resource_vocabulary()
     else:  # compatibility, aws is default for provider
         components[0] = 'aws.%s' % components[0]
         load_resources((components[0],))
-        resource_mapping = schema.resource_vocabulary('aws')
+        resource_mapping = schema.resource_vocabulary('aws', aliases=True)
 
     #
     # Handle mode
@@ -413,18 +413,21 @@ def schema_cmd(options):
     # Handle resource
     #
     resource = components[0]
-    if resource not in resource_mapping:
+    resource_info = resource_mapping.get(resource, resource_mapping['aliases'].get(resource))
+    if resource_info is None:
         log.error('{} is not a valid resource'.format(resource))
         sys.exit(1)
 
     if len(components) == 1:
         docstring = ElementSchema.doc(
-            resource_mapping[resource]['classes']['resource'])
-        del(resource_mapping[resource]['classes'])
+            resource_info['classes']['resource'])
+        resource_info.pop('classes', None)
+        # de-alias to preferred resource name
+        resource = resource_info.pop('resource_type', resource)
         if docstring:
             print("\nHelp\n----\n")
             print(docstring + '\n')
-        output = {resource: resource_mapping[resource]}
+        output = {resource: resource_info}
         print(yaml_dump(output))
         return
 
@@ -438,9 +441,9 @@ def schema_cmd(options):
 
     if len(components) == 2:
         output = "No {} available for resource {}.".format(category, resource)
-        if category in resource_mapping[resource]:
+        if category in resource_info:
             output = {resource: {
-                category: resource_mapping[resource][category]}}
+                category: resource_info[category]}}
         print(yaml_dump(output))
         return
 
@@ -448,12 +451,12 @@ def schema_cmd(options):
     # Handle item
     #
     item = components[2]
-    if item not in resource_mapping[resource][category]:
+    if item not in resource_info[category]:
         log.error('{} is not in the {} list for resource {}'.format(item, category, resource))
         sys.exit(1)
 
     if len(components) == 3:
-        cls = resource_mapping[resource]['classes'][category][item]
+        cls = resource_info['classes'][category][item]
         _print_cls_schema(cls)
 
         return
diff --git a/c7n/filters/core.py b/c7n/filters/core.py
index 5617f5688..84aee5ef4 100644
--- a/c7n/filters/core.py
+++ b/c7n/filters/core.py
@@ -35,6 +35,7 @@ from c7n.executor import ThreadPoolExecutor
 from c7n.registry import PluginRegistry
 from c7n.resolver import ValuesFrom
 from c7n.utils import set_annotation, type_schema, parse_cidr
+from c7n.manager import iter_filters
 
 
 class FilterValidationError(Exception):
@@ -140,11 +141,11 @@ class FilterRegistry(PluginRegistry):
         if isinstance(data, dict) and len(data) == 1 and 'type' not in data:
             op = list(data.keys())[0]
             if op == 'or':
-                return Or(data, self, manager)
+                return self['or'](data, self, manager)
             elif op == 'and':
-                return And(data, self, manager)
+                return self['and'](data, self, manager)
             elif op == 'not':
-                return Not(data, self, manager)
+                return self['not'](data, self, manager)
             return ValueFilter(data, manager)
         if isinstance(data, str):
             filter_type = data
@@ -164,6 +165,26 @@ class FilterRegistry(PluginRegistry):
                     self.plugin_type, data))
 
 
+def trim_runtime(filters):
+    """Remove runtime filters.
+
+    Some filters can only be effectively evaluated at policy
+    execution, ie. event filters.
+
+    When evaluating conditions for dryrun or provisioning stages we
+    remove them.
+    """
+    def remove_filter(f):
+        block = f.get_block_parent()
+        block.filters.remove(f)
+        if isinstance(block, BooleanGroupFilter) and not len(block):
+            remove_filter(block)
+
+    for f in iter_filters(filters):
+        if isinstance(f, EventFilter):
+            remove_filter(f)
+
+
 # Really should be an abstract base class (abc) or
 # zope.interface
 
@@ -198,16 +219,21 @@ class Filter(Element):
     def get_block_operator(self):
         """Determine the immediate parent boolean operator for a filter"""
         # Top level operator is `and`
-        block_stack = ['and']
+        block = self.get_block_parent()
+        if block.type in ('and', 'or', 'not'):
+            return block.type
+        return 'and'
+
+    def get_block_parent(self):
+        """Get the block parent for a filter"""
+        block_stack = [self.manager]
         for f in self.manager.iter_filters(block_end=True):
             if f is None:
                 block_stack.pop()
-                continue
-            if f.type in ('and', 'or', 'not'):
-                block_stack.append(f.type)
-            if f == self:
-                break
-        return block_stack[-1]
+            elif f == self:
+                return block_stack[-1]
+            elif f.type in ('and', 'or', 'not'):
+                block_stack.append(f)
 
     def merge_annotation(self, r, annotation_key, values):
         block_op = self.get_block_operator()
@@ -249,6 +275,12 @@ class BooleanGroupFilter(Filter):
         resource_type = self.manager.get_model()
         return resource_type.id
 
+    def __len__(self):
+        return len(self.filters)
+
+    def __bool__(self):
+        return True
+
 
 class Or(BooleanGroupFilter):
 
@@ -577,7 +609,7 @@ class ValueFilter(Filter):
         return False
 
     def process_value_type(self, sentinel, value, resource):
-        if self.vtype == 'normalize':
+        if self.vtype == 'normalize' and isinstance(value, str):
             return sentinel, value.strip().lower()
 
         elif self.vtype == 'expr':
diff --git a/c7n/manager.py b/c7n/manager.py
index eb57b047a..b0995227c 100644
--- a/c7n/manager.py
+++ b/c7n/manager.py
@@ -28,6 +28,18 @@ except ImportError:
 from c7n.utils import dumps
 
 
+def iter_filters(filters, block_end=False):
+    queue = deque(filters)
+    while queue:
+        f = queue.popleft()
+        if f is not None and f.type in ('or', 'and', 'not'):
+            if block_end:
+                queue.appendleft(None)
+            for gf in f.filters:
+                queue.appendleft(gf)
+        yield f
+
+
 class ResourceManager:
     """
     A Cloud Custodian resource
@@ -121,15 +133,7 @@ class ResourceManager:
         return self.query.resolve(self.resource_type)
 
     def iter_filters(self, block_end=False):
-        queue = deque(self.filters)
-        while queue:
-            f = queue.popleft()
-            if f and f.type in ('or', 'and', 'not'):
-                if block_end:
-                    queue.appendleft(None)
-                for gf in f.filters:
-                    queue.appendleft(gf)
-            yield f
+        return iter_filters(self.filters, block_end=block_end)
 
     def validate(self):
         """
diff --git a/c7n/policy.py b/c7n/policy.py
index f644fef00..63b9b9d2b 100644
--- a/c7n/policy.py
+++ b/c7n/policy.py
@@ -26,6 +26,7 @@ from c7n.cwe import CloudWatchEvents
 from c7n.ctx import ExecutionContext
 from c7n.exceptions import PolicyValidationError, ClientError, ResourceLimitExceeded
 from c7n.filters import FilterRegistry, And, Or, Not
+from c7n.manager import iter_filters
 from c7n.output import DEFAULT_NAMESPACE, NullBlobOutput
 from c7n.resources import load_resources
 from c7n.registry import PluginRegistry
@@ -917,8 +918,7 @@ class PolicyConditions:
 
     def validate(self):
         self.filters.extend(self.convert_deprecated())
-        self.filters = self.filter_registry.parse(
-            self.filters, self.policy.resource_manager)
+        self.filters = self.filter_registry.parse(self.filters, self)
 
     def evaluate(self, event=None):
         policy_vars = dict(self.env_vars)
@@ -931,6 +931,7 @@ class PolicyConditions:
             'now': datetime.utcnow().replace(tzinfo=tzutil.tzutc()),
             'policy': self.policy.data
         })
+
         # note for no filters/conditions, this uses all([]) == true property.
         state = all([f.process([policy_vars], event) for f in self.filters])
         if not state:
@@ -938,6 +939,9 @@ class PolicyConditions:
                 'Skipping policy:%s due to execution conditions', self.policy.name)
         return state
 
+    def iter_filters(self, block_end=False):
+        return iter_filters(self.filters, block_end=block_end)
+
     def convert_deprecated(self):
         filters = []
         if 'region' in self.policy.data:
@@ -1141,9 +1145,18 @@ class Policy:
             permissions.update(a.get_permissions())
         return permissions
 
+    def _trim_runtime_filters(self):
+        from c7n.filters.core import trim_runtime
+        trim_runtime(self.conditions.filters)
+        trim_runtime(self.resource_manager.filters)
+
     def __call__(self):
         """Run policy in default mode"""
         mode = self.get_execution_mode()
+        if (isinstance(mode, ServerlessExecutionMode) or
+                self.options.dryrun):
+            self._trim_runtime_filters()
+
         if self.options.dryrun:
             resources = PullMode(self).run()
         elif not self.is_runnable():
diff --git a/c7n/schema.py b/c7n/schema.py
index e3ecbb364..a6169d6a1 100644
--- a/c7n/schema.py
+++ b/c7n/schema.py
@@ -438,10 +438,13 @@ def resource_outline(provider=None):
     return outline
 
 
-def resource_vocabulary(cloud_name=None, qualify_name=True):
+def resource_vocabulary(cloud_name=None, qualify_name=True, aliases=True):
     vocabulary = {}
     resources = {}
 
+    if aliases:
+        vocabulary['aliases'] = {}
+
     for cname, ctype in clouds.items():
         if cloud_name is not None and cloud_name != cname:
             continue
@@ -471,6 +474,15 @@ def resource_vocabulary(cloud_name=None, qualify_name=True):
             'classes': classes,
         }
 
+        if aliases and resource_type.type_aliases:
+            provider = type_name.split('.', 1)[0]
+            for type_alias in resource_type.type_aliases:
+                vocabulary['aliases'][
+                    "{}.{}".format(provider, type_alias)] = vocabulary[type_name]
+                if provider == 'aws':
+                    vocabulary['aliases'][type_alias] = vocabulary[type_name]
+            vocabulary[type_name]['resource_type'] = type_name
+
     vocabulary["mode"] = {}
     for mode_name, cls in execution.items():
         vocabulary["mode"][mode_name] = cls


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 9a42419ac..6adc1a9e9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -178,6 +178,16 @@ class SchemaTest(CliTest):
             sorted(data['aws']['aws.ec2'].keys()), ['actions', 'filters'])
         self.assertTrue(len(data['aws']['aws.ec2']['actions']) > 10)
 
+    def test_schema_alias(self):
+        stdout, stderr = self.run_and_expect_success([
+            "custodian", "schema", "aws.network-addr"])
+        self.assertIn("aws.elastic-ip:", stdout)
+
+    def test_schema_alias_unqualified(self):
+        stdout, stderr = self.run_and_expect_success([
+            "custodian", "schema", "network-addr"])
+        self.assertIn("aws.elastic-ip:", stdout)
+
     def test_schema(self):
 
         # no options
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 8ced947d9..3dfe2c605 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -11,8 +11,10 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from copy import deepcopy
 from c7n.ctx import ExecutionContext
 from c7n.filters import Filter
+from c7n.filters.core import trim_runtime
 from c7n.resources.ec2 import EC2
 from c7n.tags import Tag
 from .common import BaseTest, instance, Bag
@@ -46,6 +48,33 @@ class TestEC2Manager(BaseTest):
             [f.type for f in p.resource_manager.iter_filters()],
             ['and', 'listener', 'listener'])
 
+    def test_trim_runtime_filters(self):
+        filter_data = [
+            {'and': [
+                {'not': [{
+                    'type': 'event',
+                    'key': 'xyz',
+                    'value': 'bar'}]},
+                {'key': 'value'}]}
+        ]
+
+        p = self.load_policy({
+            'name': 'xyz',
+            'resource': 'aws.ec2',
+            'mode': {
+                'type': 'config-rule',
+                'role': 'xyz'},
+            'filters': deepcopy(filter_data)})
+        m = p.resource_manager
+        trim_runtime(m.filters)
+        self.assertEqual(
+            [n is not None and n.type or n for n in m.iter_filters(
+                block_end=True)],
+            ['and', 'value', None])
+        # we modify filters array in place on resource manager
+        # but we don't touch the underlying policy data structure
+        self.assertEqual(m.data['filters'], filter_data)
+
     def test_filter_get_block_op(self):
         class F(Filter):
             type = 'xyz'
@@ -60,6 +89,11 @@ class TestEC2Manager(BaseTest):
             ]})
 
         m = p.resource_manager
+        self.assertEqual(
+            [n is not None and n.type or n for n in m.iter_filters(
+                block_end=True)],
+            ['and', 'or', None, None, 'not', None, 'or', None])
+
         f = F({}, m)
         m.filters.append(f)
         self.assertEqual(f.get_block_operator(), 'and')
diff --git a/tests/test_policy.py b/tests/test_policy.py
index 51d114534..7896183fb 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -11,6 +11,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from copy import deepcopy
 from datetime import datetime, timedelta
 import json
 import logging
@@ -25,7 +26,7 @@ from c7n.exceptions import ResourceLimitExceeded, PolicyValidationError
 from c7n.resources import aws, load_available
 from c7n.resources.aws import AWS, fake_session
 from c7n.resources.ec2 import EC2
-from c7n.policy import ConfigPollRuleMode
+from c7n.policy import ConfigPollRuleMode, PullMode
 from c7n.schema import generate, JsonSchemaValidator
 from c7n.utils import dumps
 from c7n.query import ConfigSource, TypeInfo
@@ -1046,6 +1047,45 @@ class PolicyConditionsTest(BaseTest):
                     {'region': 'us-east-1'}]}]})
         self.assertFalse(p.is_runnable())
 
+    def test_dryrun_event_filter(self):
+        pdata = {
+            'name': 'manga',
+            'resource': 'aws.ec2',
+            'mode': {
+                'type': 'config-rule',
+                'role': 'something'
+            },
+            'filters': [{
+                'not': [
+                    {'type': 'event'}
+                ]
+            }]
+        }
+        self.patch(PullMode, 'run', lambda self: [True])
+        p = self.load_policy(
+            deepcopy(pdata), config={'dryrun': True})
+        results = p.run()
+        self.assertEqual(results, [True])
+        self.assertTrue(p.is_runnable())
+        self.assertEqual(pdata, p.data)
+
+    def test_boolean_not_event(self):
+        # event is bound to execution evaluation, when
+        # evaluating conditions for provisioning we
+        # strip any event filters.
+        pdata = {
+            'name': 'manga',
+            'resource': 'aws.ec2',
+            'conditions': [{
+                'or': [
+                    {'not': [
+                        {'type': 'event'}]}]}]}
+        p = self.load_policy(pdata)
+        p._trim_runtime_filters()
+        self.assertTrue(p.is_runnable())
+        self.assertFalse(p.conditions.filters)
+        self.assertEqual(p.data, pdata)
+
 
 class PolicyExecutionModeTest(BaseTest):