Repository: aws/aws-cli

Issue:
cloudformation package - add support to S3 location for AWS::AppSync::Resolver
Hi there,

I think it would be a cool feature to see [AWS::AppSync::Resolver](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-resolver.html)'s S3 locations
```
{
  "ResponseMappingTemplateS3Location" : String,
  ...
  "RequestMappingTemplateS3Location" : String,
}
```
to support upload to an S3 bucket like [AWS::AppSync::GraphQLSchema](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlschema.html)'s `DefinitionS3Location` does.

Thanks :kissing: 


Discussion:
We would love this as well! Right now we have to use aws s3 sync to manage the files ourselves, and it makes for templates that are easy to break.

The referenced #3487 would be even more flexible, of course.
Seems like a reasonable feature request. @sanathkr - what are your thoughts? 
I (wrongfully) created an issue about this on the SAM repo (https://github.com/awslabs/serverless-application-model/issues/572) ; so please add my vote on this one 🙂 

Resolving patch:
diff --git a/awscli/customizations/cloudformation/artifact_exporter.py b/awscli/customizations/cloudformation/artifact_exporter.py
--- a/awscli/customizations/cloudformation/artifact_exporter.py
+++ b/awscli/customizations/cloudformation/artifact_exporter.py
@@ -218,6 +218,7 @@ class Resource(object):
     Base class representing a CloudFormation resource that can be exported
     """
 
+    RESOURCE_TYPE = None
     PROPERTY_NAME = None
     PACKAGE_NULL_PROPERTY = True
     # Set this property to True in base class if you want the exporter to zip
@@ -306,11 +307,13 @@ def do_export(self, resource_id, resource_dict, parent_dir):
 
 
 class ServerlessFunctionResource(Resource):
+    RESOURCE_TYPE = "AWS::Serverless::Function"
     PROPERTY_NAME = "CodeUri"
     FORCE_ZIP = True
 
 
 class ServerlessApiResource(Resource):
+    RESOURCE_TYPE = "AWS::Serverless::Api"
     PROPERTY_NAME = "DefinitionUri"
     # Don't package the directory if DefinitionUri is omitted.
     # Necessary to support DefinitionBody
@@ -318,10 +321,28 @@ class ServerlessApiResource(Resource):
 
 
 class GraphQLSchemaResource(Resource):
+    RESOURCE_TYPE = "AWS::AppSync::GraphQLSchema"
     PROPERTY_NAME = "DefinitionS3Location"
 
 
+class AppSyncResolverRequestTemplateResource(Resource):
+    RESOURCE_TYPE = "AWS::AppSync::Resolver"
+    PROPERTY_NAME = "RequestMappingTemplateS3Location"
+    # Don't package the directory if RequestMappingTemplateS3Location is omitted.
+    # Necessary to support RequestMappingTemplate
+    PACKAGE_NULL_PROPERTY = False
+
+
+class AppSyncResolverResponseTemplateResource(Resource):
+    RESOURCE_TYPE = "AWS::AppSync::Resolver"
+    PROPERTY_NAME = "ResponseMappingTemplateS3Location"
+    # Don't package the directory if ResponseMappingTemplateS3Location is omitted.
+    # Necessary to support ResponseMappingTemplate
+    PACKAGE_NULL_PROPERTY = False
+
+
 class LambdaFunctionResource(ResourceWithS3UrlDict):
+    RESOURCE_TYPE = "AWS::Lambda::Function"
     PROPERTY_NAME = "Code"
     BUCKET_NAME_PROPERTY = "S3Bucket"
     OBJECT_KEY_PROPERTY = "S3Key"
@@ -330,6 +351,7 @@ class LambdaFunctionResource(ResourceWithS3UrlDict):
 
 
 class ApiGatewayRestApiResource(ResourceWithS3UrlDict):
+    RESOURCE_TYPE = "AWS::ApiGateway::RestApi"
     PROPERTY_NAME = "BodyS3Location"
     PACKAGE_NULL_PROPERTY = False
     BUCKET_NAME_PROPERTY = "Bucket"
@@ -338,6 +360,7 @@ class ApiGatewayRestApiResource(ResourceWithS3UrlDict):
 
 
 class ElasticBeanstalkApplicationVersion(ResourceWithS3UrlDict):
+    RESOURCE_TYPE = "AWS::ElasticBeanstalk::ApplicationVersion"
     PROPERTY_NAME = "SourceBundle"
     BUCKET_NAME_PROPERTY = "S3Bucket"
     OBJECT_KEY_PROPERTY = "S3Key"
@@ -349,6 +372,7 @@ class CloudFormationStackResource(Resource):
     Represents CloudFormation::Stack resource that can refer to a nested
     stack template via TemplateURL property.
     """
+    RESOURCE_TYPE = "AWS::CloudFormation::Stack"
     PROPERTY_NAME = "TemplateURL"
 
     def __init__(self, uploader):
@@ -393,17 +417,17 @@ def do_export(self, resource_id, resource_dict, parent_dir):
                     parts["Key"], parts.get("Version", None))
 
 
-EXPORT_DICT = {
-    "AWS::Serverless::Function": ServerlessFunctionResource,
-    "AWS::Serverless::Api": ServerlessApiResource,
-    "AWS::AppSync::GraphQLSchema": GraphQLSchemaResource,
-    "AWS::ApiGateway::RestApi": ApiGatewayRestApiResource,
-    "AWS::Lambda::Function": LambdaFunctionResource,
-    "AWS::ElasticBeanstalk::ApplicationVersion":
-        ElasticBeanstalkApplicationVersion,
-    "AWS::CloudFormation::Stack": CloudFormationStackResource
-}
-
+EXPORT_LIST = [
+    ServerlessFunctionResource,
+    ServerlessApiResource,
+    GraphQLSchemaResource,
+    AppSyncResolverRequestTemplateResource,
+    AppSyncResolverResponseTemplateResource,
+    ApiGatewayRestApiResource,
+    LambdaFunctionResource,
+    ElasticBeanstalkApplicationVersion,
+    CloudFormationStackResource
+]
 
 class Template(object):
     """
@@ -411,7 +435,7 @@ class Template(object):
     """
 
     def __init__(self, template_path, parent_dir, uploader,
-                 resources_to_export=EXPORT_DICT):
+                 resources_to_export=EXPORT_LIST):
         """
         Reads the template and makes it ready for export
         """
@@ -448,10 +472,12 @@ def export(self):
             resource_type = resource.get("Type", None)
             resource_dict = resource.get("Properties", None)
 
-            if resource_type in self.resources_to_export:
+            for exporter_class in self.resources_to_export:
+                if exporter_class.RESOURCE_TYPE != resource_type:
+                    continue
+
                 # Export code resources
-                exporter = self.resources_to_export[resource_type](
-                        self.uploader)
+                exporter = exporter_class(self.uploader)
                 exporter.export(resource_id, resource_dict, self.template_dir)
 
         return self.template_dict


Test patch:
diff --git a/tests/unit/customizations/cloudformation/test_artifact_exporter.py b/tests/unit/customizations/cloudformation/test_artifact_exporter.py
--- a/tests/unit/customizations/cloudformation/test_artifact_exporter.py
+++ b/tests/unit/customizations/cloudformation/test_artifact_exporter.py
@@ -715,16 +715,18 @@ def test_template_export(self, yaml_parse_mock):
         template_str = self.example_yaml_template()
 
         resource_type1_class = Mock()
+        resource_type1_class.RESOURCE_TYPE = "resource_type1"
         resource_type1_instance = Mock()
         resource_type1_class.return_value = resource_type1_instance
         resource_type2_class = Mock()
+        resource_type2_class.RESOURCE_TYPE = "resource_type2"
         resource_type2_instance = Mock()
         resource_type2_class.return_value = resource_type2_instance
 
-        resources_to_export = {
-            "resource_type1": resource_type1_class,
-            "resource_type2": resource_type2_class
-        }
+        resources_to_export = [
+            resource_type1_class,
+            resource_type2_class
+        ]
 
         properties = {"foo": "bar"}
         template_dict = {