Repository: cloud-custodian/cloud-custodian

Issue:
azure - add a check for Function periodic syntax
## Actual
If you add a poorly formatted or cron syntax, you will get a host runtime failure in the Azure Function. This can be fairly difficult to debug at face value.

Ex: '* * * * *' (missing one time interval)

## Expected
We should do validation of the syntax to ensure it will be compatible with the Azure Function. We can throw a validation exception here.



Discussion:
Can do a regex in the json schema for both azure function and container host mode (note one is a 6 part and one is a 5 part).
### ACCEPTANCE CRITERIA

1. When validating a policy we are able to check if a schedule schema is correct.

FYI @aluong 
@stefangordon I will do this one, can you assign me?

Resolving patch:
diff --git a/tools/c7n_azure/c7n_azure/container_host/modes.py b/tools/c7n_azure/c7n_azure/container_host/modes.py
--- a/tools/c7n_azure/c7n_azure/container_host/modes.py
+++ b/tools/c7n_azure/c7n_azure/container_host/modes.py
@@ -49,8 +49,16 @@ def provision(self):
 @execution.register(CONTAINER_TIME_TRIGGER_MODE)
 class AzureContainerPeriodicMode(AzureContainerHostMode, PullMode):
     """A policy that runs at specified time intervals."""
+    # Pattern based on apscheduler's CronTrigger:
+    # https://github.com/agronholm/apscheduler/tree/master/apscheduler/triggers/cron
+    schedule_regex = (r'^\s?(\*|[0-9]|\,|\/|\-)+ '
+                      r'(\*|[0-9]|\,|\/|\-)+ '
+                      r'(\*|[1-9]|[1-2][0-9]|3[0-1]|\,|\*\/|\-)+ '
+                      r'(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|'
+                      r'\,|\*\/|[1-9]|1[0-2]|\*)+ '
+                      r'(mon|tue|wed|thu|fri|sat|sun|[0-6]|\,|\*|\-)+\s?$')
     schema = utils.type_schema(CONTAINER_TIME_TRIGGER_MODE,
-                               schedule={'type': 'string'},
+                               schedule={'type': 'string', 'pattern': schedule_regex},
                                rinherit=AzureContainerHostMode.schema)
 
     def provision(self):
diff --git a/tools/c7n_azure/c7n_azure/policy.py b/tools/c7n_azure/c7n_azure/policy.py
--- a/tools/c7n_azure/c7n_azure/policy.py
+++ b/tools/c7n_azure/c7n_azure/policy.py
@@ -293,8 +293,19 @@ def run_for_event(policy, event=None):
 class AzurePeriodicMode(AzureFunctionMode, PullMode):
     """A policy that runs/execute s in azure functions at specified
     time intervals."""
+    # Based on NCRONTAB used by Azure Functions:
+    # https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer
+    schedule_regex = (r'^\s?([0-5]?[0-9]|\,|(\*\/)|\-)+ '
+                      r'(\*|[0-5]?[0-9]|\,|\/|\-)+ '
+                      r'(\*|[0-9]|(1[0-9])|(2[0-3])|\,|\/|\-)+ '
+                      r'(\*|[1-9]|([1-2][0-9])|(3[0-1])|\,|\*\/|\-)+ '
+                      r'([Jj](an|anuary)|[Ff](eb|ebruary)|[Mm](ar|arch)|[Aa](pr|pril)|[Mm]ay|'
+                      r'[Jj](un|une)|[Jj](ul|uly)|[Aa](ug|ugust)|[Ss](ep|eptember)|[Oo](ct'
+                      r'|ctober)|[Nn](ov|ovember)|[Dd](ec|ecember)|\,|\*\/|[1-9]|(1[0-2])|\*)+ '
+                      r'([Mm](on|onday)|[Tt](u|ue|ues|uesday)|[Ww](ed|ednesday)|[Tt](hu|hursday)|'
+                      r'[Ff](ri|riday)|[Ss](at|aturday)|[Ss](un|unday)|[0-6]|\,|\*|\-)+\s?$')
     schema = utils.type_schema(FUNCTION_TIME_TRIGGER_MODE,
-                               schedule={'type': 'string'},
+                               schedule={'type': 'string', 'pattern': schedule_regex},
                                rinherit=AzureFunctionMode.schema)
 
     def provision(self):


Test patch:
diff --git a/tools/c7n_azure/tests/test_function_package.py b/tools/c7n_azure/tests/test_function_package.py
--- a/tools/c7n_azure/tests/test_function_package.py
+++ b/tools/c7n_azure/tests/test_function_package.py
@@ -39,7 +39,7 @@ def test_add_function_config_periodic(self):
             'resource': 'azure.publicip',
             'mode':
                 {'type': FUNCTION_TIME_TRIGGER_MODE,
-                 'schedule': '0 1 0 0 0'}
+                 'schedule': '0 1 0 1 1 1'}
         })
 
         packer = FunctionPackage(p.data['name'])
@@ -50,7 +50,7 @@ def test_add_function_config_periodic(self):
 
         self.assertEqual(binding['bindings'][0]['type'], 'timerTrigger')
         self.assertEqual(binding['bindings'][0]['name'], 'input')
-        self.assertEqual(binding['bindings'][0]['schedule'], '0 1 0 0 0')
+        self.assertEqual(binding['bindings'][0]['schedule'], '0 1 0 1 1 1')
 
     def test_add_function_config_events(self):
         p = self.load_policy({
diff --git a/tools/c7n_azure/tests/test_policy.py b/tools/c7n_azure/tests/test_policy_mode.py
similarity index 86%
rename from tools/c7n_azure/tests/test_policy.py
rename to tools/c7n_azure/tests/test_policy_mode.py
--- a/tools/c7n_azure/tests/test_policy.py
+++ b/tools/c7n_azure/tests/test_policy_mode.py
@@ -18,6 +18,7 @@
 from c7n_azure.constants import FUNCTION_EVENT_TRIGGER_MODE, FUNCTION_TIME_TRIGGER_MODE, \
     CONTAINER_EVENT_TRIGGER_MODE, CONTAINER_TIME_TRIGGER_MODE
 from c7n_azure.policy import AzureEventGridMode, AzureFunctionMode, AzureModeCommon
+from jsonschema import ValidationError
 from mock import mock, patch, Mock
 
 
@@ -55,7 +56,7 @@ def test_azure_function_periodic_mode_schema_validation(self):
                 'resource': 'azure.vm',
                 'mode':
                     {'type': FUNCTION_TIME_TRIGGER_MODE,
-                     'schedule': '0 * /5 * * * *',
+                     'schedule': '0 */5 * * * *',
                      'provision-options': {
                          'servicePlan': {
                              'name': 'test-cloud-custodian',
@@ -71,6 +72,99 @@ def test_azure_function_periodic_mode_schema_validation(self):
             })
             self.assertTrue(p)
 
+    def test_azure_function_periodic_schema_schedule_valid(self):
+        policy = {
+            'name': 'test-azure-schema-schedule-valid',
+            'resource': 'azure.vm',
+            'mode': {
+                'type': FUNCTION_TIME_TRIGGER_MODE,
+                'schedule': ''
+            }
+        }
+
+        valid_schedules = [
+            '0 5 */2 * * friday',
+            '0 * 5 * February *',
+            '5-7 * * * * 1-5',
+            '5,8,10 * * * Jan Mon'
+        ]
+
+        result = True
+        for valid_schedule in valid_schedules:
+            policy['mode']['schedule'] = valid_schedule
+            p = self.load_policy(policy, validate=True)
+            result = result and p
+
+        self.assertTrue(result)
+
+    def test_azure_function_periodic_schema_schedule_invalid(self):
+        policy = {
+            'name': 'test-azure-schema-schedule-invalid',
+            'resource': 'azure.vm',
+            'mode': {
+                'type': FUNCTION_TIME_TRIGGER_MODE,
+                'schedule': ''
+            }
+        }
+
+        invalid_schedules = [
+            '* * * * *',
+            '0 * * * * * *',
+            '* * * * * *',
+            '0 0 0 0 0 0',
+            '15-60 * * * * 7'
+        ]
+
+        for invalid_schedule in invalid_schedules:
+            policy['mode']['schedule'] = invalid_schedule
+            with self.assertRaises(ValidationError):
+                self.load_policy(policy, validate=True)
+
+    def test_container_periodic_schema_schedule_valid(self):
+        policy = {
+            'name': 'test-azure-periodic-mode',
+            'resource': 'azure.vm',
+            'mode':
+                {'type': CONTAINER_TIME_TRIGGER_MODE,
+                    'schedule': ''}
+        }
+
+        valid_schedules = [
+            '5 */2 * * fri',
+            ' * 5 * feb * ',
+            '5-7 * * * 1-5 ',
+            '5,8,10 * * jan mon'
+        ]
+
+        result = True
+        for valid_schedule in valid_schedules:
+            policy['mode']['schedule'] = valid_schedule
+            p = self.load_policy(policy, validate=True)
+            result = result and p
+
+        self.assertTrue(result)
+
+    def test_container_periodic_schema_schedule_invalid(self):
+        policy = {
+            'name': 'test-azure-periodic-mode',
+            'resource': 'azure.vm',
+            'mode':
+                {'type': CONTAINER_TIME_TRIGGER_MODE,
+                    'schedule': ''}
+        }
+
+        invalid_schedules = [
+            '* * * *',
+            '* * * * * *'
+            '*/15 * Jan 1-5',
+            '* 15 * jan 7',
+        ]
+
+        for invalid_schedule in invalid_schedules:
+            policy['mode']['schedule'] = invalid_schedule
+            with self.assertRaises(ValidationError):
+                self.load_policy(policy, validate=True)
+
     def test_container_event_mode_schema_validation(self):
         with self.sign_out_patch():
             p = self.load_policy({
@@ -89,7 +183,7 @@ def test_container_periodic_mode_schema_validation(self):
                 'resource': 'azure.vm',
                 'mode':
                     {'type': CONTAINER_TIME_TRIGGER_MODE,
-                     'schedule': '* /5 * * * *'}
+                     'schedule': '*/5 * * * *'}
             })
             self.assertTrue(p)