1from samtranslator.public.sdk.resource import SamResourceType
2from samtranslator.public.intrinsics import is_intrinsics
3from samtranslator.swagger.swagger import SwaggerEditor
4from six import string_types
5
6
7class Globals(object):
8    """
9    Class to parse and process Globals section in SAM template. If a property is specified at Global section for
10    say Function, then this class will add it to each resource of AWS::Serverless::Function type.
11    """
12
13    # Key of the dictionary containing Globals section in SAM template
14    _KEYWORD = "Globals"
15    _RESOURCE_PREFIX = "AWS::Serverless::"
16    _OPENAPIVERSION = "OpenApiVersion"
17    _API_TYPE = "AWS::Serverless::Api"
18    _MANAGE_SWAGGER = "__MANAGE_SWAGGER"
19
20    supported_properties = {
21        # Everything on Serverless::Function except Role, Policies, FunctionName, Events
22        SamResourceType.Function.value: [
23            "Handler",
24            "Runtime",
25            "CodeUri",
26            "DeadLetterQueue",
27            "Description",
28            "MemorySize",
29            "Timeout",
30            "VpcConfig",
31            "Environment",
32            "Tags",
33            "Tracing",
34            "KmsKeyArn",
35            "AutoPublishAlias",
36            "Layers",
37            "DeploymentPreference",
38            "PermissionsBoundary",
39            "ReservedConcurrentExecutions",
40            "ProvisionedConcurrencyConfig",
41            "AssumeRolePolicyDocument",
42            "EventInvokeConfig",
43            "FileSystemConfigs",
44            "CodeSigningConfigArn",
45            "Architectures",
46        ],
47        # Everything except
48        #   DefinitionBody: because its hard to reason about merge of Swagger dictionaries
49        #   StageName: Because StageName cannot be overridden for Implicit APIs because of the current plugin
50        #              architecture
51        SamResourceType.Api.value: [
52            "Auth",
53            "Name",
54            "DefinitionUri",
55            "CacheClusterEnabled",
56            "CacheClusterSize",
57            "Variables",
58            "EndpointConfiguration",
59            "MethodSettings",
60            "BinaryMediaTypes",
61            "MinimumCompressionSize",
62            "Cors",
63            "GatewayResponses",
64            "AccessLogSetting",
65            "CanarySetting",
66            "TracingEnabled",
67            "OpenApiVersion",
68            "Domain",
69        ],
70        SamResourceType.HttpApi.value: [
71            "Auth",
72            "AccessLogSettings",
73            "StageVariables",
74            "Tags",
75            "CorsConfiguration",
76            "DefaultRouteSettings",
77            "Domain",
78            "RouteSettings",
79            "FailOnWarnings",
80        ],
81        SamResourceType.SimpleTable.value: ["SSESpecification"],
82    }
83
84    def __init__(self, template):
85        """
86        Constructs an instance of this object
87
88        :param dict template: SAM template to be parsed
89        """
90        self.supported_resource_section_names = [
91            x.replace(self._RESOURCE_PREFIX, "") for x in self.supported_properties.keys()
92        ]
93        # Sort the names for stability in list ordering
94        self.supported_resource_section_names.sort()
95
96        self.template_globals = {}
97
98        if self._KEYWORD in template:
99            self.template_globals = self._parse(template[self._KEYWORD])
100
101    def merge(self, resource_type, resource_properties):
102        """
103        Adds global properties to the resource, if necessary. This method is a no-op if there are no global properties
104        for this resource type
105
106        :param string resource_type: Type of the resource (Ex: AWS::Serverless::Function)
107        :param dict resource_properties: Properties of the resource that need to be merged
108        :return dict: Merged properties of the resource
109        """
110
111        if resource_type not in self.template_globals:
112            # Nothing to do. Return the template unmodified
113            return resource_properties
114
115        global_props = self.template_globals[resource_type]
116
117        return global_props.merge(resource_properties)
118
119    @classmethod
120    def del_section(cls, template):
121        """
122        Helper method to delete the Globals section altogether from the template
123
124        :param dict template: SAM template
125        :return: Modified SAM template with Globals section
126        """
127
128        if cls._KEYWORD in template:
129            del template[cls._KEYWORD]
130
131    @classmethod
132    def fix_openapi_definitions(cls, template):
133        """
134        Helper method to postprocess the resources to make sure the swagger doc version matches
135        the one specified on the resource with flag OpenApiVersion.
136
137        This is done postprocess in globals because, the implicit api plugin runs before globals, \
138        and at that point the global flags aren't applied on each resource, so we do not know \
139        whether OpenApiVersion flag is specified. Running the globals plugin before implicit api \
140        was a risky change, so we decided to postprocess the openapi version here.
141
142        To make sure we don't modify customer defined swagger, we also check for __MANAGE_SWAGGER flag.
143
144        :param dict template: SAM template
145        :return: Modified SAM template with corrected swagger doc matching the OpenApiVersion.
146        """
147        resources = template.get("Resources", {})
148
149        for _, resource in resources.items():
150            if ("Type" in resource) and (resource["Type"] == cls._API_TYPE):
151                properties = resource["Properties"]
152                if (
153                    (cls._OPENAPIVERSION in properties)
154                    and (cls._MANAGE_SWAGGER in properties)
155                    and SwaggerEditor.safe_compare_regex_with_string(
156                        SwaggerEditor.get_openapi_version_3_regex(), properties[cls._OPENAPIVERSION]
157                    )
158                ):
159                    if not isinstance(properties[cls._OPENAPIVERSION], string_types):
160                        properties[cls._OPENAPIVERSION] = str(properties[cls._OPENAPIVERSION])
161                        resource["Properties"] = properties
162                    if "DefinitionBody" in properties:
163                        definition_body = properties["DefinitionBody"]
164                        definition_body["openapi"] = properties[cls._OPENAPIVERSION]
165                        if definition_body.get("swagger"):
166                            del definition_body["swagger"]
167
168    def _parse(self, globals_dict):
169        """
170        Takes a SAM template as input and parses the Globals section
171
172        :param globals_dict: Dictionary representation of the Globals section
173        :return: Processed globals dictionary which can be used to quickly identify properties to merge
174        :raises: InvalidResourceException if the input contains properties that we don't support
175        """
176
177        globals = {}
178        if not isinstance(globals_dict, dict):
179            raise InvalidGlobalsSectionException(
180                self._KEYWORD, "It must be a non-empty dictionary".format(self._KEYWORD)
181            )
182
183        for section_name, properties in globals_dict.items():
184            resource_type = self._make_resource_type(section_name)
185
186            if resource_type not in self.supported_properties:
187                raise InvalidGlobalsSectionException(
188                    self._KEYWORD,
189                    "'{section}' is not supported. "
190                    "Must be one of the following values - {supported}".format(
191                        section=section_name, supported=self.supported_resource_section_names
192                    ),
193                )
194
195            if not isinstance(properties, dict):
196                raise InvalidGlobalsSectionException(self._KEYWORD, "Value of ${section} must be a dictionary")
197
198            for key, value in properties.items():
199                supported = self.supported_properties[resource_type]
200                if key not in supported:
201                    raise InvalidGlobalsSectionException(
202                        self._KEYWORD,
203                        "'{key}' is not a supported property of '{section}'. "
204                        "Must be one of the following values - {supported}".format(
205                            key=key, section=section_name, supported=supported
206                        ),
207                    )
208
209            # Store all Global properties in a map with key being the AWS::Serverless::* resource type
210            globals[resource_type] = GlobalProperties(properties)
211
212        return globals
213
214    def _make_resource_type(self, key):
215        return self._RESOURCE_PREFIX + key
216
217
218class GlobalProperties(object):
219    """
220    Object holding the global properties of given type. It also contains methods to perform a merge between
221    Global & resource-level properties. Here are the different cases during the merge and how we handle them:
222
223    **Primitive Type (String, Integer, Boolean etc)**
224    If either global & local are of primitive types, then we the value at local will overwrite global.
225
226    Example:
227
228      ```
229      Global:
230        Function:
231          Runtime: nodejs
232
233      Function:
234         Runtime: python
235      ```
236
237    After processing, Function resource will contain:
238      ```
239      Runtime: python
240      ```
241
242    **Different data types**
243    If a value at Global is a array, but local is a dictionary, then we will simply use the local value.
244    There is no merge to be done here. Similarly for other data type mismatches between global & local value.
245
246    Example:
247
248      ```
249      Global:
250        Function:
251          CodeUri: s3://bucket/key
252
253      Function:
254         CodeUri:
255           Bucket: foo
256           Key: bar
257      ```
258
259
260    After processing, Function resource will contain:
261      ```
262        CodeUri:
263           Bucket: foo
264           Key: bar
265      ```
266
267    **Arrays**
268    If a value is an array at both global & local level, we will simply concatenate them without de-duplicating.
269    Customers can easily fix the duplicates:
270
271    Example:
272
273      ```
274       Global:
275         Function:
276           Policy: [Policy1, Policy2]
277
278       Function:
279         Policy: [Policy1, Policy3]
280      ```
281
282    After processing, Function resource will contain:
283    (notice the duplicates)
284      ```
285       Policy: [Policy1, Policy2, Policy1, Policy3]
286      ```
287
288    **Dictionaries**
289    If both global & local value is a dictionary, we will recursively merge properties. If a value is one of the above
290    types, they will handled according the above rules.
291
292    Example:
293
294      ```
295       Global:
296         EnvironmentVariables:
297           TableName: foo
298           DBName: generic-db
299
300       Function:
301          EnvironmentVariables:
302            DBName: mydb
303            ConnectionString: bar
304      ```
305
306    After processing, Function resource will contain:
307      ```
308          EnvironmentVariables:
309            TableName: foo
310            DBName: mydb
311            ConnectionString: bar
312      ```
313
314    ***Optional Properties***
315    Some resources might have optional properties with default values when it is skipped. If an optional property
316    is skipped at local level, an explicitly specified value at global level will be used.
317
318    Example:
319      Global:
320        DeploymentPreference:
321           Enabled: False
322           Type: Canary
323
324      Function:
325        DeploymentPreference:
326          Type: Linear
327
328    After processing, Function resource will contain:
329      ```
330      DeploymentPreference:
331         Enabled: False
332         Type: Linear
333      ```
334    (in other words, Deployments will be turned off for the Function)
335
336    """
337
338    def __init__(self, global_properties):
339        self.global_properties = global_properties
340
341    def merge(self, local_properties):
342        """
343        Merge Global & local level properties according to the above rules
344
345        :return local_properties: Dictionary of local properties
346        """
347        return self._do_merge(self.global_properties, local_properties)
348
349    def _do_merge(self, global_value, local_value):
350        """
351        Actually perform the merge operation for the given inputs. This method is used as part of the recursion.
352        Therefore input values can be of any type. So is the output.
353
354        :param global_value: Global value to be merged
355        :param local_value: Local value to be merged
356        :return: Merged result
357        """
358
359        token_global = self._token_of(global_value)
360        token_local = self._token_of(local_value)
361
362        # The following statements codify the rules explained in the doctring above
363        if token_global != token_local:
364            return self._prefer_local(global_value, local_value)
365
366        elif self.TOKEN.PRIMITIVE == token_global == token_local:
367            return self._prefer_local(global_value, local_value)
368
369        elif self.TOKEN.DICT == token_global == token_local:
370            return self._merge_dict(global_value, local_value)
371
372        elif self.TOKEN.LIST == token_global == token_local:
373            return self._merge_lists(global_value, local_value)
374
375        else:
376            raise TypeError(
377                "Unsupported type of objects. GlobalType={}, LocalType={}".format(token_global, token_local)
378            )
379
380    def _merge_lists(self, global_list, local_list):
381        """
382        Merges the global list with the local list. List merging is simply a concatenation = global + local
383
384        :param global_list: Global value list
385        :param local_list: Local value list
386        :return: New merged list with the elements shallow copied
387        """
388
389        return global_list + local_list
390
391    def _merge_dict(self, global_dict, local_dict):
392        """
393        Merges the two dictionaries together
394
395        :param global_dict: Global dictionary to be merged
396        :param local_dict: Local dictionary to be merged
397        :return: New merged dictionary with values shallow copied
398        """
399
400        # Local has higher priority than global. So iterate over local dict and merge into global if keys are overridden
401        global_dict = global_dict.copy()
402
403        for key in local_dict.keys():
404
405            if key in global_dict:
406                # Both local & global contains the same key. Let's do a merge.
407                global_dict[key] = self._do_merge(global_dict[key], local_dict[key])
408
409            else:
410                # Key is not in globals, just in local. Copy it over
411                global_dict[key] = local_dict[key]
412
413        return global_dict
414
415    def _prefer_local(self, global_value, local_value):
416        """
417        Literally returns the local value whatever it may be. This method is useful to provide a unified implementation
418        for cases that don't require special handling.
419
420        :param global_value: Global value
421        :param local_value: Local value
422        :return: Simply returns the local value
423        """
424        return local_value
425
426    def _token_of(self, input):
427        """
428        Returns the token type of the input.
429
430        :param input: Input whose type is to be determined
431        :return TOKENS: Token type of the input
432        """
433
434        if isinstance(input, dict):
435
436            # Intrinsic functions are always dicts
437            if is_intrinsics(input):
438                # Intrinsic functions are handled *exactly* like a primitive type because
439                # they resolve to a primitive type when creating a stack with CloudFormation
440                return self.TOKEN.PRIMITIVE
441            else:
442                return self.TOKEN.DICT
443
444        elif isinstance(input, list):
445            return self.TOKEN.LIST
446
447        else:
448            return self.TOKEN.PRIMITIVE
449
450    class TOKEN:
451        """
452        Enum of tokens used in the merging
453        """
454
455        PRIMITIVE = "primitive"
456        DICT = "dict"
457        LIST = "list"
458
459
460class InvalidGlobalsSectionException(Exception):
461    """Exception raised when a Globals section is is invalid.
462
463    Attributes:
464        message -- explanation of the error
465    """
466
467    def __init__(self, logical_id, message):
468        self._logical_id = logical_id
469        self._message = message
470
471    @property
472    def message(self):
473        return "'{}' section is invalid. {}".format(self._logical_id, self._message)
474