Repository: cloud-custodian/cloud-custodian

Issue:
Azure - Resource tagging for App service plans not functioning
it looks like the action of tagging was removed from app service plans in #4154 - was this intentional? I use the following policy which now does not work: 
```
vars:
  email-notify: &email-notify
  - type: notify
    template: my_default.html
    priority_header: '2'
    to:
      - me@company.com
    transport:
      type: asq
      queue: https://storageQueue

  schedules:
    mode: &azure-12h
      type: azure-periodic
      schedule: 0 0 */12 * * *
      execution-options:
          output_dir: azure://storage_Queue
  tag-sets:
    - contact-missing: &contact-missing
      - tag:OwnerContact: absent
      - tag:OwnerContact: empty
    - env-product-owner-tags: &env-product-owner-tags
      - tag:Environment: empty
      - tag:Product: empty
      - tag:Owner: empty
-policies:
- name: ownercontact-absent-appserviceplan
  mode: *azure-12h
  resource: azure.appserviceplan
  filters:
  - or: *contact-missing
  actions:
  - type: tag
    tags:
      OwnerContact: ''
```
This results in an error of: 
```
c7n.exceptions.PolicyValidationError: Failed to validate policy ownercontact-absent-appserviceplan
 Error on policy:ownercontact-absent-appserviceplan resource:azure.appserviceplan
{'type': 'tag', 'tags': {'OwnerContact': ''}} is not valid under any of the given schemas

Failed validating 'anyOf' in schema[198]['allOf'][1]['properties']['actions']['items']:
    {'anyOf': [{'$ref': '#/definitions/actions/azure.delete'},
               {'$ref': '#/definitions/actions/azure.lock'},
               {'$ref': '#/definitions/actions/azure.logic-app'},
               {'$ref': '#/definitions/actions/azure.notify'},
               {'$ref': '#/definitions/resources/azure.appserviceplan/actions/resize-plan'},
               {'$ref': '#/definitions/actions/azure.webhook'},
               {'enum': ['notify',
                         'webhook',
                         'resize-plan',
                         'logic-app',
                         'lock',
                         'delete']}]}
```


Discussion:
if the action of tagging this resource should be performed differently, please point out, thanks.
@stefangordon @logachev ^
We removed it due to a bug in the App Service Plan REST API which prevents tagging safely, but people need this so we'll need to code in some sort of work around and get it working somehow rather than waiting for the API to be fixed.   

For context, the Azure CLI tags these resources by doing a full resource GET followed by a full resource POST - we wanted to avoid that as there is a significant chance of you being underwritten.  Currently we only tag resources that implement PATCH (update specific fields only).  

I'm tagging/adding this to the backlog.  

Notes for who this gets assigned to:
Verify if we can still PATCH tags on this resource as long as we provide the non-standard additional fields (SKU?) that it requires, if so, add appropriate conditions or resource specific tagging overrides to make this happen.

If not, we need a way to fall back to a GET/POST tagging instead of PATCH, and we need to enable it on this resource. 
Related #3327

Resolving patch:
diff --git a/tools/c7n_azure/c7n_azure/resources/appserviceplan.py b/tools/c7n_azure/c7n_azure/resources/appserviceplan.py
--- a/tools/c7n_azure/c7n_azure/resources/appserviceplan.py
+++ b/tools/c7n_azure/c7n_azure/resources/appserviceplan.py
@@ -53,7 +53,6 @@ class resource_type(ArmResourceManager.resource_type):
             'kind'
         )
         resource_type = 'Microsoft.Web/sites'
-        enable_tag_operations = False
 
 
 @AppServicePlan.action_registry.register('resize-plan')
diff --git a/tools/c7n_azure/c7n_azure/resources/sqldatabase.py b/tools/c7n_azure/c7n_azure/resources/sqldatabase.py
--- a/tools/c7n_azure/c7n_azure/resources/sqldatabase.py
+++ b/tools/c7n_azure/c7n_azure/resources/sqldatabase.py
@@ -59,6 +59,7 @@ class resource_type(ChildArmResourceManager.resource_type):
         enum_spec = ('databases', 'list_by_server', None)
         parent_manager_name = 'sqlserver'
         resource_type = 'Microsoft.Sql/servers/databases'
+        enable_tag_operations = False  # GH Issue #4543
 
         @classmethod
         def extra_args(cls, parent_resource):
diff --git a/tools/c7n_azure/c7n_azure/tags.py b/tools/c7n_azure/c7n_azure/tags.py
--- a/tools/c7n_azure/c7n_azure/tags.py
+++ b/tools/c7n_azure/c7n_azure/tags.py
@@ -43,15 +43,10 @@ def update_resource_tags(tag_action, resource, tags):
                                           .format(az_resource.type))
             api_version = tag_action.session.resource_api_version(resource['id'])
 
-            # create a GenericResource object with the required parameters
-            generic_resource = GenericResource(location=az_resource.location,
-                                               tags=tags,
-                                               properties=az_resource.properties,
-                                               kind=az_resource.kind,
-                                               managed_by=az_resource.managed_by,
-                                               identity=az_resource.identity)
-
-            client.resources.update_by_id(resource['id'], api_version, generic_resource)
+            # create a PATCH object with only updates to tags
+            tags_patch = GenericResource(tags=tags)
+
+            client.resources.update_by_id(resource['id'], api_version, tags_patch)
 
     @staticmethod
     def remove_tags(tag_action, resource, tags_to_delete):


Test patch:
diff --git a/tools/c7n_azure/tests/templates/cleanup.sh b/tools/c7n_azure/tests/templates/cleanup.sh
--- a/tools/c7n_azure/tests/templates/cleanup.sh
+++ b/tools/c7n_azure/tests/templates/cleanup.sh
@@ -14,7 +14,7 @@ for file in "$templateDirectory"/*.json; do
 
   if [ $# -eq 0 ] || [[ "$@" =~ "$filenameNoExtension" ]]; then
     echo "Deleting $rgName"
-    az group delete --name $rgName --yes
+    az group delete --name $rgName --yes --no-wait
   else
     echo "Skipping $rgName"
   fi
@@ -24,14 +24,14 @@ done
 # Destroy ACS resource
 rgName=test_containerservice
 if [ $# -eq 0 ] || [[ "$@" =~ "containerservice" ]]; then
-  az group delete --name $rgName --yes
+  az group delete --name $rgName --yes --no-wait
 else
   echo "Skipping $rgName"
 fi
 
 # Destroy Azure Policy Assignment
 if [ $# -eq 0 ] || [[ "$@" =~ "policyassignment" ]]; then
-  az policy assignment delete --name cctestpolicy
+  az policy assignment delete --name cctestpolicy --no-wait
 else
   echo "Skipping policyassignment"
 fi
\ No newline at end of file
diff --git a/tools/c7n_azure/tests/templates/provision.sh b/tools/c7n_azure/tests/templates/provision.sh
--- a/tools/c7n_azure/tests/templates/provision.sh
+++ b/tools/c7n_azure/tests/templates/provision.sh
@@ -35,9 +35,9 @@ for file in "$templateDirectory"/*.json; do
         az keyvault key create --vault-name $vault_name --name cctestrsa --kty RSA
         az keyvault key create --vault-name $vault_name --name cctestec --kty EC
       elif [[ "$filenameNoExtension" =~ "aks" ]]; then
-        az group deployment create --resource-group $rgName --template-file $file --parameters client_id=$AZURE_CLIENT_ID client_secret=$AZURE_CLIENT_SECRET
+        az group deployment create --resource-group $rgName --template-file $file --parameters client_id=$AZURE_CLIENT_ID client_secret=$AZURE_CLIENT_SECRET --no-wait
       else
-        az group deployment create --resource-group $rgName --template-file $file
+        az group deployment create --resource-group $rgName --template-file $file --no-wait
       fi
   else
     echo "Skipping $rgName"
diff --git a/tools/c7n_azure/tests/test_tags.py b/tools/c7n_azure/tests/test_tags.py
--- a/tools/c7n_azure/tests/test_tags.py
+++ b/tools/c7n_azure/tests/test_tags.py
@@ -72,6 +72,8 @@ def test_update_tags(self):
         args = client_mock.resource_groups.update.call_args[0]
         self.assertEqual(args[0], resource_group['name'])
         self.assertEqual(args[1].tags, self.existing_tags)
+        # Only PATCH tags
+        self.assertListEqual(['tags'], [x for x in args[1].as_dict() if x is not None])
 
         action.manager.type = 'vm'
         TagHelper.update_resource_tags(action, resource, self.existing_tags)
@@ -79,3 +81,5 @@ def test_update_tags(self):
         args = client_mock.resources.update_by_id.call_args[0]
         self.assertEqual(args[0], resource['id'])
         self.assertEqual(args[2].tags, self.existing_tags)
+        # Only PATCH tags
+        self.assertListEqual(['tags'], [x for x in args[2].as_dict() if x is not None])