Repository: cloud-custodian/cloud-custodian

Issue:
Azure - update policies in Azure Function doesn't work. 
I tried to modify policies and redeploy again. The command get "INFO Function publish result: 202" but the functions is still running old policies. 
For example, policies with actions:
 actions:                                                                                                                                                                                                                      
   - type: tag                                                                                                                                                                                                                    
      tag: Hello04a                                                                                                                                                                                                          
      value: World04a
Confirmed it worked. Then modified the actions to :
  actions:                                                                                                                                                                                                                      
   - type: tag                                                                                                                                                                                                                    
      tag: Hello04b                                                                                                                                                                                                        
      value: World04b
And deployed again ( using: custodian run ..._) and got "INFO Function publish result: 202". 
But actually the function is still apply old tags to resources. 


Discussion:
After investigation it appears that the function is updating, but there is slow performance for tags in Azure. Let me know if you are able to still repro this and don't see the config.json updating in the function. 

Resolving patch:
diff --git a/c7n/mu.py b/c7n/mu.py
--- a/c7n/mu.py
+++ b/c7n/mu.py
@@ -39,7 +39,6 @@
 from c7n.logs_support import _timestamp_from_string
 from c7n.utils import parse_s3, local_session
 
-
 log = logging.getLogger('custodian.serverless')
 
 
@@ -100,6 +99,17 @@ def size(self):
             raise ValueError("Archive not closed, size not accurate")
         return os.stat(self._temp_archive_file.name).st_size
 
+    def create_zinfo(self, file):
+        if not isinstance(file, zipfile.ZipInfo):
+            file = zinfo(file)
+
+        # Ensure we apply the compression
+        file.compress_type = self.zip_compression
+        # Mark host OS as Linux for all archives
+        file.create_system = 3
+
+        return file
+
     def add_modules(self, ignore, modules):
         """Add the named Python modules to the archive. For consistency's sake
         we only add ``*.py`` files, not ``*.pyc``. We also don't add other
@@ -202,12 +212,7 @@ def add_contents(self, dest, contents):
 
         """
         assert not self._closed, "Archive closed"
-        if not isinstance(dest, zipfile.ZipInfo):
-            dest = zinfo(dest)  # see for some caveats
-        # Ensure we apply the compression
-        dest.compress_type = self.zip_compression
-        # Mark host OS as Linux for all archives
-        dest.create_system = 3
+        dest = self.create_zinfo(dest)
         self._zip_file.writestr(dest, contents)
 
     def close(self):
@@ -219,8 +224,7 @@ def close(self):
         self._zip_file.close()
         log.debug(
             "Created custodian serverless archive size: %0.2fmb",
-            (os.path.getsize(self._temp_archive_file.name) / (
-                1024.0 * 1024.0)))
+            (os.path.getsize(self._temp_archive_file.name) / (1024.0 * 1024.0)))
         return self
 
     def remove(self):
@@ -347,7 +351,7 @@ def func_metrics(f):
                     Dimensions=[{
                         'Name': 'FunctionName',
                         'Value': (
-                            isinstance(f, dict) and f['FunctionName'] or f.name)}],
+                                isinstance(f, dict) and f['FunctionName'] or f.name)}],
                     Statistics=["Sum"],
                     StartTime=start,
                     EndTime=end,
@@ -396,8 +400,7 @@ def delta_function(old_config, new_config):
         for k in new_config:
             # Layers need special handling as they have extra info on describe.
             if k == 'Layers' and k in old_config and new_config[k]:
-                if sorted(new_config[k]) != sorted([
-                        l['Arn'] for l in old_config[k]]):
+                if sorted(new_config[k]) != sorted([l['Arn'] for l in old_config[k]]):
                     changed.append(k)
             # Vpc needs special handling as a dict with lists
             elif k == 'VpcConfig' and k in old_config and new_config[k]:
@@ -1408,7 +1411,7 @@ class Done(Exception):
                             response = sns_client.unsubscribe(
                                 SubscriptionArn=subscription['SubscriptionArn'])
                             log.debug("Unsubscribed %s from %s" %
-                                (func.name, topic_name))
+                                      (func.name, topic_name))
                         except sns_client.exceptions.NotFoundException:
                             pass
                         raise Done  # break out of both for loops
@@ -1454,7 +1457,7 @@ def get_topic(self, bucket):
                 'TopicArn': topic_arn,
                 'Events': ['s3:ObjectCreated:*']})
             s3.put_bucket_notification_configuration(Bucket=bucket['Name'],
-                NotificationConfiguration=notifies)
+                                                     NotificationConfiguration=notifies)
             topic_arns = [topic_arn]
         return topic_arns
 
@@ -1497,10 +1500,10 @@ def get_rule_params(self, func):
                 config_type = manager.get_model().config_type
             else:
                 raise Exception("You may have attempted to deploy a config "
-                        "based lambda function with an unsupported config type. "
-                        "The most recent AWS config types are here: http://docs.aws"
-                        ".amazon.com/config/latest/developerguide/resource"
-                        "-config-reference.html.")
+                                "based lambda function with an unsupported config type. "
+                                "The most recent AWS config types are here: http://docs.aws"
+                                ".amazon.com/config/latest/developerguide/resource"
+                                "-config-reference.html.")
             params['Scope'] = {
                 'ComplianceResourceTypes': [config_type]}
         else:
diff --git a/tools/c7n_azure/c7n_azure/function_package.py b/tools/c7n_azure/c7n_azure/function_package.py
--- a/tools/c7n_azure/c7n_azure/function_package.py
+++ b/tools/c7n_azure/c7n_azure/function_package.py
@@ -20,9 +20,6 @@
 import time
 
 import requests
-
-from c7n.mu import PythonPackageArchive
-from c7n.utils import local_session
 from c7n_azure.constants import (ENV_CUSTODIAN_DISABLE_SSL_CERT_VERIFICATION,
                                  FUNCTION_EVENT_TRIGGER_MODE,
                                  FUNCTION_TIME_TRIGGER_MODE,
@@ -31,6 +28,24 @@
 from c7n_azure.dependency_manager import DependencyManager
 from c7n_azure.session import Session
 
+from c7n.mu import PythonPackageArchive
+from c7n.utils import local_session
+
+
+class AzurePythonPackageArchive(PythonPackageArchive):
+    def __init__(self, modules=(), cache_file=None):
+        super(AzurePythonPackageArchive, self).__init__(modules, cache_file)
+        self.package_time = time.gmtime()
+
+    def create_zinfo(self, file):
+        """
+        In Dedicated App Service Plans - Functions are updated via KuduSync
+        The KuduSync uses the modified time and file size to determine if a file has changed
+        """
+        info = super(AzurePythonPackageArchive, self).create_zinfo(file)
+        info.date_time = self.package_time[0:6]
+        return info
+
 
 class FunctionPackage(object):
 
@@ -130,7 +145,7 @@ def cache_folder(self):
     def build(self, policy, modules, non_binary_packages, excluded_packages, queue_name=None):
         cache_zip_file = self.build_cache(modules, excluded_packages, non_binary_packages)
 
-        self.pkg = PythonPackageArchive(cache_file=cache_zip_file)
+        self.pkg = AzurePythonPackageArchive(cache_file=cache_zip_file)
 
         self.pkg.add_modules(None, [m.replace('-', '_') for m in modules])
 
@@ -145,7 +160,7 @@ def build_cache(self, modules, excluded_packages, non_binary_packages):
         packages = DependencyManager.get_dependency_packages_list(modules, excluded_packages)
 
         if not DependencyManager.check_cache(cache_metadata_file, cache_zip_file, packages):
-            cache_pkg = PythonPackageArchive()
+            cache_pkg = AzurePythonPackageArchive()
             self.log.info("Cached packages not found or requirements were changed.")
 
             # If cache check fails, wipe all previous wheels, installations etc
@@ -186,6 +201,7 @@ def build_cache(self, modules, excluded_packages, non_binary_packages):
             DependencyManager.create_cache_metadata(cache_metadata_file,
                                                     cache_zip_file,
                                                     packages)
+
         return cache_zip_file
 
     def wait_for_status(self, deployment_creds, retries=10, delay=15):


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
@@ -15,15 +15,15 @@
 
 import json
 import os
+import time
 
 from azure_common import BaseTest
-from c7n_azure.function_package import FunctionPackage
-from c7n_azure.constants import ENV_CUSTODIAN_DISABLE_SSL_CERT_VERIFICATION,\
+from c7n_azure.function_package import FunctionPackage, AzurePythonPackageArchive
+from c7n_azure.constants import ENV_CUSTODIAN_DISABLE_SSL_CERT_VERIFICATION, \
     FUNCTION_TIME_TRIGGER_MODE, FUNCTION_EVENT_TRIGGER_MODE
 from mock import patch, MagicMock, PropertyMock
 
 from azure.mgmt.web.models.user import User
-from c7n.mu import PythonPackageArchive
 
 test_files_folder = os.path.join(os.path.dirname(__file__), 'data')
 
@@ -89,6 +89,38 @@ def test_add_policy(self):
                           u'mode': {u'type': u'azure-event-grid',
                                     u'events': [u'VmWrite']}})
 
+    def test_zipped_files_have_modified_timestamp(self):
+        t = time.gmtime(1577854800)
+        package = AzurePythonPackageArchive()
+        package.package_time = t
+        package.add_contents('test.txt', 'Hello, World')
+        package.close()
+
+        zinfo = package._zip_file.infolist()[0]
+        self.assertEqual('test.txt', zinfo.filename)
+        self.assertEqual(t[0:6], zinfo.date_time)
+
+    def test_zipped_files_with_zip_cache_unmodified_cache_timestamps(self):
+        cache_ts = time.gmtime(1577000000)
+        cache = AzurePythonPackageArchive()
+        cache.package_time = cache_ts
+        cache.add_contents('cache.txt', 'I am a cache file')
+        cache.close()
+
+        new_ts = time.gmtime(1577854800)
+        package = AzurePythonPackageArchive(cache_file=cache.path)
+        package.package_time = new_ts
+        package.add_contents('new.txt', 'I am a new file')
+        package.close()
+
+        cache_file = package._zip_file.infolist()[0]
+        self.assertEqual('cache.txt', cache_file.filename)
+        self.assertEqual(cache_ts[0:6], cache_file.date_time)
+
+        new_file = package._zip_file.infolist()[1]
+        self.assertEqual('new.txt', new_file.filename)
+        self.assertEqual(new_ts[0:6], new_file.date_time)
+
     @patch("c7n_azure.session.Session.get_functions_auth_string", return_value="")
     def test_event_package_files(self, session_mock):
         p = self.load_policy({
@@ -100,7 +132,7 @@ def test_event_package_files(self, session_mock):
         })
 
         packer = FunctionPackage(p.data['name'])
-        packer.pkg = PythonPackageArchive()
+        packer.pkg = AzurePythonPackageArchive()
 
         packer._add_functions_required_files(p.data, 'test-queue')
         files = packer.pkg._zip_file.filelist
@@ -116,7 +148,7 @@ def test_no_policy_add_required_files(self, session_mock):
         """ Tools such as mailer will package with no policy """
 
         packer = FunctionPackage('name')
-        packer.pkg = PythonPackageArchive()
+        packer.pkg = AzurePythonPackageArchive()
 
         packer._add_functions_required_files(None)
         files = packer.pkg._zip_file.filelist
@@ -125,14 +157,14 @@ def test_no_policy_add_required_files(self, session_mock):
 
     def test_add_host_config(self):
         packer = FunctionPackage('test')
-        packer.pkg = PythonPackageArchive()
-        with patch('c7n.mu.PythonPackageArchive.add_contents') as mock:
+        packer.pkg = AzurePythonPackageArchive()
+        with patch('c7n_azure.function_package.AzurePythonPackageArchive.add_contents') as mock:
             packer._add_host_config(FUNCTION_EVENT_TRIGGER_MODE)
             mock.assert_called_once()
             self.assertEqual(mock.call_args[1]['dest'], 'host.json')
             self.assertTrue('extensionBundle' in json.loads(mock.call_args[1]['contents']))
 
-        with patch('c7n.mu.PythonPackageArchive.add_contents') as mock:
+        with patch('c7n_azure.function_package.AzurePythonPackageArchive.add_contents') as mock:
             packer._add_host_config(FUNCTION_TIME_TRIGGER_MODE)
             mock.assert_called_once()
             self.assertEqual(mock.call_args[1]['dest'], 'host.json')
@@ -143,7 +175,7 @@ def test_publish(self, post_mock):
         status_mock = MagicMock()
         post_mock.return_value = status_mock
         packer = FunctionPackage('test')
-        packer.pkg = PythonPackageArchive()
+        packer.pkg = AzurePythonPackageArchive()
         creds = User(publishing_user_name='user',
                      publishing_password='password',
                      scm_uri='https://uri')
@@ -187,8 +219,10 @@ def test_package_build_no_cache(self, rmtree_mock, add_files_mock):
             mocks.append(self._create_patch(
                 'c7n_azure.dependency_manager.DependencyManager.' + f[0],
                 return_value=f[1]))
-        add_modules_mock = self._create_patch('c7n.mu.PythonPackageArchive.add_modules')
-        mocks.append(self._create_patch('c7n.mu.PythonPackageArchive.add_file'))
+        add_modules_mock = self._create_patch(
+            'c7n_azure.function_package.AzurePythonPackageArchive.add_modules')
+        mocks.append(self._create_patch(
+            'c7n_azure.function_package.AzurePythonPackageArchive.add_file'))
 
         cache_zip = os.path.join(test_files_folder, 'cache.zip')
         self.addCleanup(os.remove, cache_zip)
@@ -225,8 +259,9 @@ def test_package_build_cache(self, _1, rmtree_mock, add_files_mock):
             mocks.append(self._create_patch(
                 'c7n_azure.dependency_manager.DependencyManager.' + f[0],
                 return_value=f[1]))
-        add_modules_mock = self._create_patch('c7n.mu.PythonPackageArchive.add_modules')
-        self._create_patch('c7n.mu.PythonPackageArchive.__init__')
+        add_modules_mock = self._create_patch(
+            'c7n_azure.function_package.AzurePythonPackageArchive.add_modules')
+        self._create_patch('c7n_azure.function_package.AzurePythonPackageArchive.__init__')
 
         packer = FunctionPackage('test')
         packer.build({}, [], [], [], 'queue')
diff --git a/tools/c7n_azure/tests/test_functionapp_utils.py b/tools/c7n_azure/tests/test_functionapp_utils.py
--- a/tools/c7n_azure/tests/test_functionapp_utils.py
+++ b/tools/c7n_azure/tests/test_functionapp_utils.py
@@ -14,14 +14,13 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 from azure_common import BaseTest, arm_template
-from c7n_azure.function_package import FunctionPackage
+from c7n_azure.function_package import FunctionPackage, AzurePythonPackageArchive
 from c7n_azure.functionapp_utils import FunctionAppUtilities
 
 from c7n_azure.provisioning.app_insights import AppInsightsUnit
 from mock import patch
 
 from c7n.utils import local_session
-from c7n.mu import PythonPackageArchive
 from c7n_azure.session import Session
 
 CONST_GROUP_NAME = 'test_functionapp-reqs'
@@ -159,7 +158,7 @@ def test_publish_functions_package_consumption(self, _1):
             function_app_name='cloud-custodian-test-consumption')
 
         package = FunctionPackage("TestPolicy")
-        package.pkg = PythonPackageArchive()
+        package.pkg = AzurePythonPackageArchive()
         package.close()
 
         FunctionAppUtilities.publish_functions_package(