Repository: cloud-custodian/cloud-custodian

Issue:
Using both 'max-resources' AND 'max-resources-percent' together in policy validation
Hi there! For policy validation, what would happen if I use both 'max-resources' and 'max-resources-percent' together? I assume if either one condition is met, policy validation would fail? Is it possible such that both of those conditions have to be met in order for policy validation to fail? I'm running into use cases where I'd need to do both.

Ex:
max-resources: 2 AND max-resources-percent: 50%
max-resources: 2 OR max-resources-percent: 50%


Discussion:
do you have a more worked real world example of needing this differential behavior/configuration.  right now the behavior is `or` and there are no plans to change that.
also clarifying terms, these configuration values are not part of validation, they are a runtime execution circuit breaker. validation takes place statically pre execution and ANY api calls.
Thanks for the quick response and clarification! We have an organizational wide policy to add circuit breaker logic all custodian policies that have action that we consider destructive (delete or update). The problem with that is that we decided that the default 'max-resources-percent' would be say 10% across the board, but certain types of resources by nature wouldn't have a very large numbers (but not guaranteed to always be a small number) in an account (eg: ECR, S3) and usually wouldn't hit that 10% (1 out of like 3 is already 33.33333%), in the case having AND logic makes sense so that I can do max-resources: 2 AND max-resources-percent: 10% for a delete action.
would something like this work. so max-resource can take in a dict in addition to an int.

```policies:
  - name: my-first-policy
    resource: sns
    max-resources: 
      - percent: 20
      - amount: 5
```
Thanks i like that direction, although it doesn't actual consider/address the feature in question (ie. at the moment you can already specify both of those on a policy) it does perhaps suggest a policy configuration mechanism for them. The reality is those that i'm not sure if we should  expose operator policy configuration because the use case is typically uniform across the set of policies, or let me rephrase we can expose but also look at a blanket option.  ie. I was  considering an env var flag for max resources operator (and|or) that could configure across set of policies being executed..
ah ok so like below. 

```
  - max-resource: 5
  - max-resource-percent: 20
  - max-resource-op: and
  - name: my-first-policy
    resource: sns
    - ...
```

wasn't sure if it was better to add a flag or add an option to the existing format. i meant to do the below in my previous comment. 

```
 - max-resources: 
      - percent: 20
      - amount: 5
 - name: my-first-policy
    resource: sns
    - ...
```
i prefer your syntax of encapsulating them under a single attribute to avoid policy top level attr pollution 
awesome. will create a pr. 

Resolving patch:
diff --git a/c7n/query.py b/c7n/query.py
--- a/c7n/query.py
+++ b/c7n/query.py
@@ -25,7 +25,7 @@
 
 import jmespath
 import six
-
+import os
 
 from c7n.actions import ActionRegistry
 from c7n.exceptions import ClientError, ResourceLimitExceeded
@@ -442,19 +442,8 @@ def check_resource_limit(self, selection_count, population_count):
         filtering behind the resource manager facade for default usage.
         """
         p = self.ctx.policy
-        if isinstance(p.max_resources, int) and selection_count > p.max_resources:
-            raise ResourceLimitExceeded(
-                ("policy: %s exceeded resource limit: {limit} "
-                 "found: {selection_count}") % p.name,
-                "max-resources", p.max_resources, selection_count, population_count)
-        elif p.max_resources_percent:
-            if (population_count * (
-                    p.max_resources_percent / 100.0) < selection_count):
-                raise ResourceLimitExceeded(
-                    ("policy: %s exceeded resource limit: {limit}%% "
-                     "found: {selection_count} total: {population_count}") % p.name,
-                    "max-percent", p.max_resources_percent, selection_count, population_count)
-        return True
+        max_resource_limits = MaxResourceLimit(p, selection_count, population_count)
+        return max_resource_limits.check_resource_limits()
 
     def _get_cached_resources(self, ids):
         key = self.get_cache_key(None)
@@ -540,6 +529,60 @@ def generate_arn(self):
         return self._generate_arn
 
 
+class MaxResourceLimit(object):
+
+    C7N_MAXRES_OP = os.environ.get("C7N_MAXRES_OP", 'or')
+
+    def __init__(self, policy, selection_count, population_count):
+        self.p = policy
+        self.op = MaxResourceLimit.C7N_MAXRES_OP
+        self.selection_count = selection_count
+        self.population_count = population_count
+        self.amount = None
+        self.percentage_amount = None
+        self.percent = None
+        self._parse_policy()
+
+    def _parse_policy(self,):
+        if isinstance(self.p.max_resources, dict):
+            self.op = self.p.max_resources.get("op", MaxResourceLimit.C7N_MAXRES_OP).lower()
+            self.percent = self.p.max_resources.get("percent")
+            self.amount = self.p.max_resources.get("amount")
+
+        if isinstance(self.p.max_resources, int):
+            self.amount = self.p.max_resources
+
+        if isinstance(self.p.max_resources_percent, (int, float)):
+            self.percent = self.p.max_resources_percent
+
+        if self.percent:
+            self.percentage_amount = self.population_count * (self.percent / 100.0)
+
+    def check_resource_limits(self):
+        if self.percentage_amount and self.amount:
+            if (self.selection_count > self.amount and
+               self.selection_count > self.percentage_amount and self.op == "and"):
+                raise ResourceLimitExceeded(
+                    ("policy:%s exceeded resource-limit:{limit} and percentage-limit:%s%% "
+                     "found:{selection_count} total:{population_count}")
+                    % (self.p.name, self.percent), "max-resource and max-percent",
+                    self.amount, self.selection_count, self.population_count)
+
+        if self.amount:
+            if self.selection_count > self.amount and self.op != "and":
+                raise ResourceLimitExceeded(
+                    ("policy:%s exceeded resource-limit:{limit} "
+                     "found:{selection_count} total: {population_count}") % self.p.name,
+                    "max-resource", self.amount, self.selection_count, self.population_count)
+
+        if self.percentage_amount:
+            if self.selection_count > self.percentage_amount and self.op != "and":
+                raise ResourceLimitExceeded(
+                    ("policy:%s exceeded resource-limit:{limit}%% "
+                     "found:{selection_count} total:{population_count}") % self.p.name,
+                    "max-percent", self.percent, self.selection_count, self.population_count)
+
+
 class ChildResourceManager(QueryResourceManager):
 
     child_source = 'describe-child'
diff --git a/c7n/schema.py b/c7n/schema.py
--- a/c7n/schema.py
+++ b/c7n/schema.py
@@ -190,7 +190,10 @@ def generate(resource_types=()):
                 'start': {'format': 'date-time'},
                 'end': {'format': 'date-time'},
                 'resource': {'type': 'string'},
-                'max-resources': {'type': 'integer', 'minimum': 1},
+                'max-resources': {'anyOf': [
+                    {'type': 'integer', 'minimum': 1},
+                    {'$ref': '#/definitions/max-resources-properties'}
+                ]},
                 'max-resources-percent': {'type': 'number', 'minimum': 0, 'maximum': 100},
                 'comment': {'type': 'string'},
                 'comments': {'type': 'string'},
@@ -219,6 +222,14 @@ def generate(resource_types=()):
         },
         'policy-mode': {
             'anyOf': [e.schema for _, e in execution.items()],
+        },
+        'max-resources-properties': {
+            'type': 'object',
+            'properties': {
+                'amount': {"type": 'integer', 'minimum': 1},
+                'op': {'enum': ['or', 'and']},
+                'percent': {'type': 'number', 'minimum': 0, 'maximum': 100}
+            }
         }
     }
 


Test patch:
diff --git a/tests/test_policy.py b/tests/test_policy.py
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -595,7 +595,7 @@ def test_policy_resource_limits(self):
         self.assertRaises(ResourceLimitExceeded, p.run)
         self.assertEqual(
             output.getvalue().strip(),
-            "policy: log-delete exceeded resource limit: 2.5% found: 1 total: 1")
+            "policy:log-delete exceeded resource-limit:2.5% found:1 total:1")
         self.assertEqual(
             p.ctx.metrics.buf[0]['MetricName'], 'ResourceLimitExceeded')
 
@@ -625,6 +625,35 @@ def test_policy_resource_limits_count(self):
             session_factory=session_factory
         )
 
+    def test_policy_resource_limit_and_percent(self):
+        session_factory = self.replay_flight_data(
+            "test_policy_resource_count")
+        p = self.load_policy(
+            {
+                "name": "ecs-cluster-resource-count",
+                "resource": "ecs",
+                "max-resources": {
+                    "amount": 1,
+                    "percent": 10,
+                    "op": "and"
+                }
+            },
+            session_factory=session_factory)
+        self.assertRaises(ResourceLimitExceeded, p.run)
+        p = self.load_policy(
+            {
+                "name": "ecs-cluster-resource-count",
+                "resource": "ecs",
+                "max-resources": {
+                    "amount": 100,
+                    "percent": 10,
+                    "op": "and"
+                }
+            },
+            session_factory=session_factory)
+        resources = p.run()
+        self.assertTrue(resources)
+
     def test_policy_resource_limits_with_filter(self):
         session_factory = self.replay_flight_data(
             "test_policy_resource_count_with_filter")