Repository: cloud-custodian/cloud-custodian

Issue:
Azure - max_resource does not work 
Probably not implemented in our query.py

e.g. https://github.com/cloud-custodian/cloud-custodian/pull/2854/files#diff-9e36607e744130a1127b0d4210b50b43R436

~Also, this thing does not appear to be documented.~


Discussion:
its documented over here https://cloudcustodian.io/docs/quickstart/advanced.html#policy-resource-limits

Resolving patch:
diff --git a/tools/c7n_azure/c7n_azure/query.py b/tools/c7n_azure/c7n_azure/query.py
--- a/tools/c7n_azure/c7n_azure/query.py
+++ b/tools/c7n_azure/c7n_azure/query.py
@@ -13,20 +13,20 @@
 # limitations under the License.
 
 import logging
+
 import six
-from c7n_azure.actions.notify import Notify
-from c7n_azure.actions.logic_app import LogicAppAction
 from c7n_azure import constants
-from c7n_azure.provider import resources
+from c7n_azure.actions.logic_app import LogicAppAction
+from c7n_azure.actions.notify import Notify
 from c7n_azure.filters import ParentFilter
+from c7n_azure.provider import resources
 
 from c7n.actions import ActionRegistry
 from c7n.filters import FilterRegistry
 from c7n.manager import ResourceManager
-from c7n.query import sources
+from c7n.query import sources, MaxResourceLimit
 from c7n.utils import local_session
 
-
 log = logging.getLogger('custodian.azure.query')
 
 
@@ -232,7 +232,20 @@ def resources(self, query=None):
             resources = self.augment(self.source.get_resources(query))
             self._cache.save(cache_key, resources)
 
-        return self.filter_resources(resources)
+        resource_count = len(resources)
+        resources = self.filter_resources(resources)
+
+        # Check if we're out of a policies execution limits.
+        if self.data == self.ctx.policy.data:
+            self.check_resource_limit(len(resources), resource_count)
+        return resources
+
+    def check_resource_limit(self, selection_count, population_count):
+        """Check if policy's execution affects more resources then its limit.
+        """
+        p = self.ctx.policy
+        max_resource_limits = MaxResourceLimit(p, selection_count, population_count)
+        return max_resource_limits.check_resource_limits()
 
     def get_resources(self, resource_ids, **params):
         resource_client = self.get_client()
diff --git a/tools/c7n_gcp/c7n_gcp/query.py b/tools/c7n_gcp/c7n_gcp/query.py
--- a/tools/c7n_gcp/c7n_gcp/query.py
+++ b/tools/c7n_gcp/c7n_gcp/query.py
@@ -22,7 +22,7 @@
 from c7n.actions import ActionRegistry
 from c7n.filters import FilterRegistry
 from c7n.manager import ResourceManager
-from c7n.query import sources
+from c7n.query import sources, MaxResourceLimit
 from c7n.utils import local_session
 
 
@@ -149,7 +149,21 @@ def resources(self, query=None):
         key = self.get_cache_key(q)
         resources = self._fetch_resources(q)
         self._cache.save(key, resources)
-        return self.filter_resources(resources)
+
+        resource_count = len(resources)
+        resources = self.filter_resources(resources)
+
+        # Check if we're out of a policies execution limits.
+        if self.data == self.ctx.policy.data:
+            self.check_resource_limit(len(resources), resource_count)
+        return resources
+
+    def check_resource_limit(self, selection_count, population_count):
+        """Check if policy's execution affects more resources then its limit.
+        """
+        p = self.ctx.policy
+        max_resource_limits = MaxResourceLimit(p, selection_count, population_count)
+        return max_resource_limits.check_resource_limits()
 
     def _fetch_resources(self, query):
         try:


Test patch:
diff --git a/tools/c7n_azure/tests/cassettes/QueryResourceManagerTest.resource_limits.json b/tools/c7n_azure/tests/cassettes/QueryResourceManagerTest.resource_limits.json
new file mode 100644
--- /dev/null
+++ b/tools/c7n_azure/tests/cassettes/QueryResourceManagerTest.resource_limits.json
@@ -0,0 +1,89 @@
+{
+    "version": 1,
+    "interactions": [
+        {
+            "request": {
+                "method": "GET",
+                "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups?api-version=2018-05-01",
+                "body": null,
+                "headers": {}
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "date": [
+                        "Thu, 22 Aug 2019 01:31:04 GMT"
+                    ],
+                    "content-length": [
+                        "4845"
+                    ],
+                    "cache-control": [
+                        "no-cache"
+                    ],
+                    "content-type": [
+                        "application/json; charset=utf-8"
+                    ]
+                },
+                "body": {
+                    "data": {
+                        "value": [
+                            {
+                                "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/cloud-custodian",
+                                "name": "cloud-custodian",
+                                "location": "eastus",
+                                "properties": {
+                                    "provisioningState": "Succeeded"
+                                }
+                            },
+                            {
+                                "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/cloud-custodian-emailer",
+                                "name": "cloud-custodian-emailer",
+                                "location": "westus2",
+                                "properties": {
+                                    "provisioningState": "Succeeded"
+                                }
+                            },
+                            {
+                                "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/custodian-cask",
+                                "name": "custodian-cask",
+                                "location": "westus",
+                                "tags": {},
+                                "properties": {
+                                    "provisioningState": "Succeeded"
+                                }
+                            },
+                            {
+                                "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/custodian-sendgrid",
+                                "name": "custodian-sendgrid",
+                                "location": "centralus",
+                                "tags": {},
+                                "properties": {
+                                    "provisioningState": "Succeeded"
+                                }
+                            },
+                            {
+                                "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/DefaultResourceGroup-EUS",
+                                "name": "DefaultResourceGroup-EUS",
+                                "location": "eastus",
+                                "properties": {
+                                    "provisioningState": "Succeeded"
+                                }
+                            },
+                            {
+                                "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_emptyrg",
+                                "name": "test_emptyrg",
+                                "location": "southcentralus",
+                                "properties": {
+                                    "provisioningState": "Succeeded"
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tools/c7n_azure/tests/test_azure_query.py b/tools/c7n_azure/tests/test_azure_query.py
new file mode 100644
--- /dev/null
+++ b/tools/c7n_azure/tests/test_azure_query.py
@@ -0,0 +1,68 @@
+# Copyright 2019 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# 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 __future__ import absolute_import, division, print_function, unicode_literals
+
+import logging
+
+from azure_common import BaseTest, arm_template
+from azure_common import cassette_name
+from mock import mock
+
+from c7n.exceptions import ResourceLimitExceeded
+
+
+class QueryResourceManagerTest(BaseTest):
+
+    @arm_template('emptyrg.json')
+    @cassette_name('resource_limits')
+    def test_policy_resource_limits(self):
+        p = self.load_policy(
+            {
+                "name": "limits",
+                "resource": "azure.resourcegroup",
+                "max-resources-percent": 2.5,
+                'filters': [
+                    {'type': 'value',
+                     'key': 'name',
+                     'op': 'eq',
+                     'value': 'test_emptyrg'}]
+            },
+            validate=True)
+
+        p.ctx.metrics.flush = mock.MagicMock()
+        output = self.capture_logging('custodian.policy', level=logging.ERROR)
+        self.assertRaises(ResourceLimitExceeded, p.run)
+        self.assertTrue("policy:limits exceeded resource-limit:2.5% found:1 total:"
+                        in output.getvalue())
+        self.assertEqual(
+            p.ctx.metrics.buf[0]['MetricName'], 'ResourceLimitExceeded')
+
+    @arm_template('emptyrg.json')
+    @cassette_name('resource_limits')
+    def test_policy_resource_limits_count(self):
+        p = self.load_policy(
+            {
+                "name": "limits",
+                "resource": "azure.resourcegroup",
+                "max-resources": 1,
+            },
+            validate=True)
+
+        p.ctx.metrics.flush = mock.MagicMock()
+        output = self.capture_logging('custodian.policy', level=logging.ERROR)
+        self.assertRaises(ResourceLimitExceeded, p.run)
+        self.assertTrue("policy:limits exceeded resource-limit:1 found:"
+                        in output.getvalue())
+        self.assertEqual(
+            p.ctx.metrics.buf[0]['MetricName'], 'ResourceLimitExceeded')
diff --git a/tools/c7n_gcp/tests/test_resourcemanager.py b/tools/c7n_gcp/tests/test_resourcemanager.py
--- a/tools/c7n_gcp/tests/test_resourcemanager.py
+++ b/tools/c7n_gcp/tests/test_resourcemanager.py
@@ -11,8 +11,50 @@
 # 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.
+import logging
 
 from gcp_common import BaseTest
+from mock import mock
+
+from c7n.exceptions import ResourceLimitExceeded
+
+
+class LimitsTest(BaseTest):
+    def test_policy_resource_limits(self):
+        parent = 'organizations/926683928810'
+        session_factory = self.replay_flight_data('folder-query')
+
+        p = self.load_policy(
+            {'name': 'limits',
+             "max-resources-percent": 2.5,
+             'resource': 'gcp.folder',
+             'query':
+                 [{'parent': parent}]},
+            session_factory=session_factory)
+
+        p.ctx.metrics.flush = mock.MagicMock()
+        output = self.capture_logging('custodian.policy', level=logging.ERROR)
+        self.assertRaises(ResourceLimitExceeded, p.run)
+        self.assertTrue("policy:limits exceeded resource-limit:2.5% found:1 total:"
+                        in output.getvalue())
+        self.assertEqual(
+            p.ctx.metrics.buf[0]['MetricName'], 'ResourceLimitExceeded')
+
+    def test_policy_resource_limits_count(self):
+        session_factory = self.replay_flight_data('disk-query')
+        p = self.load_policy(
+            {'name': 'limits',
+             'resource': 'gcp.disk',
+             'max-resources': 1},
+            session_factory=session_factory)
+
+        p.ctx.metrics.flush = mock.MagicMock()
+        output = self.capture_logging('custodian.policy', level=logging.ERROR)
+        self.assertRaises(ResourceLimitExceeded, p.run)
+        self.assertTrue("policy:limits exceeded resource-limit:1 found:"
+                        in output.getvalue())
+        self.assertEqual(
+            p.ctx.metrics.buf[0]['MetricName'], 'ResourceLimitExceeded')
 
 
 class OrganizationTest(BaseTest):