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