1import re 2from collections import namedtuple 3from six import string_types 4from samtranslator.model.intrinsics import ref, fnGetAtt 5from samtranslator.model.apigatewayv2 import ( 6 ApiGatewayV2HttpApi, 7 ApiGatewayV2Stage, 8 ApiGatewayV2Authorizer, 9 ApiGatewayV2DomainName, 10 ApiGatewayV2ApiMapping, 11) 12from samtranslator.model.exceptions import InvalidResourceException 13from samtranslator.model.s3_utils.uri_parser import parse_s3_uri 14from samtranslator.open_api.open_api import OpenApiEditor 15from samtranslator.translator import logical_id_generator 16from samtranslator.model.tags.resource_tagging import get_tag_list 17from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_no_value 18from samtranslator.model.route53 import Route53RecordSetGroup 19 20_CORS_WILDCARD = "*" 21CorsProperties = namedtuple( 22 "_CorsProperties", ["AllowMethods", "AllowHeaders", "AllowOrigins", "MaxAge", "ExposeHeaders", "AllowCredentials"] 23) 24CorsProperties.__new__.__defaults__ = (None, None, None, None, None, False) 25 26AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"]) 27AuthProperties.__new__.__defaults__ = (None, None) 28DefaultStageName = "$default" 29HttpApiTagName = "httpapi:createdBy" 30 31 32class HttpApiGenerator(object): 33 def __init__( 34 self, 35 logical_id, 36 stage_variables, 37 depends_on, 38 definition_body, 39 definition_uri, 40 stage_name, 41 tags=None, 42 auth=None, 43 cors_configuration=None, 44 access_log_settings=None, 45 route_settings=None, 46 default_route_settings=None, 47 resource_attributes=None, 48 passthrough_resource_attributes=None, 49 domain=None, 50 fail_on_warnings=False, 51 description=None, 52 disable_execute_api_endpoint=None, 53 ): 54 """Constructs an API Generator class that generates API Gateway resources 55 56 :param logical_id: Logical id of the SAM API Resource 57 :param stage_variables: API Gateway Variables 58 :param depends_on: Any resources that need to be depended on 59 :param definition_body: API definition 60 :param definition_uri: URI to API definition 61 :param name: Name of the API Gateway resource 62 :param stage_name: Name of the Stage 63 :param tags: Stage and API Tags 64 :param access_log_settings: Whether to send access logs and where for Stage 65 :param resource_attributes: Resource attributes to add to API resources 66 :param passthrough_resource_attributes: Attributes such as `Condition` that are added to derived resources 67 :param description: Description of the API Gateway resource 68 """ 69 self.logical_id = logical_id 70 self.stage_variables = stage_variables 71 self.depends_on = depends_on 72 self.definition_body = definition_body 73 self.definition_uri = definition_uri 74 self.stage_name = stage_name 75 if not self.stage_name: 76 self.stage_name = DefaultStageName 77 self.auth = auth 78 self.cors_configuration = cors_configuration 79 self.tags = tags 80 self.access_log_settings = access_log_settings 81 self.route_settings = route_settings 82 self.default_route_settings = default_route_settings 83 self.resource_attributes = resource_attributes 84 self.passthrough_resource_attributes = passthrough_resource_attributes 85 self.domain = domain 86 self.fail_on_warnings = fail_on_warnings 87 self.description = description 88 self.disable_execute_api_endpoint = disable_execute_api_endpoint 89 90 def _construct_http_api(self): 91 """Constructs and returns the ApiGatewayV2 HttpApi. 92 93 :returns: the HttpApi to which this SAM Api corresponds 94 :rtype: model.apigatewayv2.ApiGatewayHttpApi 95 """ 96 http_api = ApiGatewayV2HttpApi(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes) 97 98 if self.definition_uri and self.definition_body: 99 raise InvalidResourceException( 100 self.logical_id, "Specify either 'DefinitionUri' or 'DefinitionBody' property and not both." 101 ) 102 if self.cors_configuration: 103 # call this method to add cors in open api 104 self._add_cors() 105 106 self._add_auth() 107 self._add_tags() 108 109 if self.fail_on_warnings: 110 http_api.FailOnWarnings = self.fail_on_warnings 111 112 if self.disable_execute_api_endpoint is not None: 113 self._add_endpoint_configuration() 114 115 self._add_description() 116 117 if self.definition_uri: 118 http_api.BodyS3Location = self._construct_body_s3_dict() 119 elif self.definition_body: 120 http_api.Body = self.definition_body 121 else: 122 raise InvalidResourceException( 123 self.logical_id, 124 "'DefinitionUri' or 'DefinitionBody' are required properties of an " 125 "'AWS::Serverless::HttpApi'. Add a value for one of these properties or " 126 "add a 'HttpApi' event to an 'AWS::Serverless::Function'.", 127 ) 128 129 return http_api 130 131 def _add_endpoint_configuration(self): 132 """Add disableExecuteApiEndpoint if it is set in SAM 133 HttpApi doesn't have vpcEndpointIds 134 135 Note: 136 DisableExecuteApiEndpoint as a property of AWS::ApiGatewayV2::Api needs both DefinitionBody and 137 DefinitionUri to be None. However, if neither DefinitionUri nor DefinitionBody are specified, 138 SAM will generate a openapi definition body based on template configuration. 139 https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-definitionbody 140 For this reason, we always put DisableExecuteApiEndpoint into openapi object. 141 142 """ 143 if self.disable_execute_api_endpoint and not self.definition_body: 144 raise InvalidResourceException( 145 self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property." 146 ) 147 editor = OpenApiEditor(self.definition_body) 148 149 # if DisableExecuteApiEndpoint is set in both definition_body and as a property, 150 # SAM merges and overrides the disableExecuteApiEndpoint in definition_body with headers of 151 # "x-amazon-apigateway-endpoint-configuration" 152 editor.add_endpoint_config(self.disable_execute_api_endpoint) 153 154 # Assign the OpenApi back to template 155 self.definition_body = editor.openapi 156 157 def _add_cors(self): 158 """ 159 Add CORS configuration if CORSConfiguration property is set in SAM. 160 Adds CORS configuration only if DefinitionBody is present and 161 APIGW extension for CORS is not present in the DefinitionBody 162 """ 163 164 if self.cors_configuration and not self.definition_body: 165 raise InvalidResourceException( 166 self.logical_id, "Cors works only with inline OpenApi specified in 'DefinitionBody' property." 167 ) 168 169 # If cors configuration is set to true add * to the allow origins. 170 # This also support referencing the value as a parameter 171 if isinstance(self.cors_configuration, bool): 172 # if cors config is true add Origins as "'*'" 173 properties = CorsProperties(AllowOrigins=[_CORS_WILDCARD]) 174 175 elif is_intrinsic(self.cors_configuration): 176 # Just set Origin property. Intrinsics will be handledOthers will be defaults 177 properties = CorsProperties(AllowOrigins=self.cors_configuration) 178 179 elif isinstance(self.cors_configuration, dict): 180 # Make sure keys in the dict are recognized 181 if not all(key in CorsProperties._fields for key in self.cors_configuration.keys()): 182 raise InvalidResourceException(self.logical_id, "Invalid value for 'Cors' property.") 183 184 properties = CorsProperties(**self.cors_configuration) 185 186 else: 187 raise InvalidResourceException(self.logical_id, "Invalid value for 'Cors' property.") 188 189 if not OpenApiEditor.is_valid(self.definition_body): 190 raise InvalidResourceException( 191 self.logical_id, 192 "Unable to add Cors configuration because " 193 "'DefinitionBody' does not contain a valid " 194 "OpenApi definition.", 195 ) 196 197 if properties.AllowCredentials is True and properties.AllowOrigins == [_CORS_WILDCARD]: 198 raise InvalidResourceException( 199 self.logical_id, 200 "Unable to add Cors configuration because " 201 "'AllowCredentials' can not be true when " 202 "'AllowOrigin' is \"'*'\" or not set.", 203 ) 204 205 editor = OpenApiEditor(self.definition_body) 206 # if CORS is set in both definition_body and as a CorsConfiguration property, 207 # SAM merges and overrides the cors headers in definition_body with headers of CorsConfiguration 208 editor.add_cors( 209 properties.AllowOrigins, 210 properties.AllowHeaders, 211 properties.AllowMethods, 212 properties.ExposeHeaders, 213 properties.MaxAge, 214 properties.AllowCredentials, 215 ) 216 217 # Assign the OpenApi back to template 218 self.definition_body = editor.openapi 219 220 def _construct_api_domain(self, http_api): 221 """ 222 Constructs and returns the ApiGateway Domain and BasepathMapping 223 """ 224 if self.domain is None: 225 return None, None, None 226 227 if self.domain.get("DomainName") is None or self.domain.get("CertificateArn") is None: 228 raise InvalidResourceException( 229 self.logical_id, "Custom Domains only works if both DomainName and CertificateArn" " are provided." 230 ) 231 232 self.domain["ApiDomainName"] = "{}{}".format( 233 "ApiGatewayDomainNameV2", logical_id_generator.LogicalIdGenerator("", self.domain.get("DomainName")).gen() 234 ) 235 236 domain = ApiGatewayV2DomainName( 237 self.domain.get("ApiDomainName"), attributes=self.passthrough_resource_attributes 238 ) 239 domain_config = dict() 240 domain.DomainName = self.domain.get("DomainName") 241 domain.Tags = self.tags 242 endpoint = self.domain.get("EndpointConfiguration") 243 244 if endpoint is None: 245 endpoint = "REGIONAL" 246 # to make sure that default is always REGIONAL 247 self.domain["EndpointConfiguration"] = "REGIONAL" 248 elif endpoint not in ["REGIONAL"]: 249 raise InvalidResourceException( 250 self.logical_id, 251 "EndpointConfiguration for Custom Domains must be one of {}.".format(["REGIONAL"]), 252 ) 253 domain_config["EndpointType"] = endpoint 254 domain_config["CertificateArn"] = self.domain.get("CertificateArn") 255 if self.domain.get("SecurityPolicy", None): 256 domain_config["SecurityPolicy"] = self.domain.get("SecurityPolicy") 257 258 domain.DomainNameConfigurations = [domain_config] 259 260 mutual_tls_auth = self.domain.get("MutualTlsAuthentication", None) 261 if mutual_tls_auth: 262 if isinstance(mutual_tls_auth, dict): 263 if not set(mutual_tls_auth.keys()).issubset({"TruststoreUri", "TruststoreVersion"}): 264 invalid_keys = [] 265 for key in mutual_tls_auth.keys(): 266 if key not in {"TruststoreUri", "TruststoreVersion"}: 267 invalid_keys.append(key) 268 invalid_keys.sort() 269 raise InvalidResourceException( 270 ",".join(invalid_keys), 271 "Available MutualTlsAuthentication fields are {}.".format( 272 ["TruststoreUri", "TruststoreVersion"] 273 ), 274 ) 275 domain.MutualTlsAuthentication = {} 276 if mutual_tls_auth.get("TruststoreUri", None): 277 domain.MutualTlsAuthentication["TruststoreUri"] = mutual_tls_auth["TruststoreUri"] 278 if mutual_tls_auth.get("TruststoreVersion", None): 279 domain.MutualTlsAuthentication["TruststoreVersion"] = mutual_tls_auth["TruststoreVersion"] 280 else: 281 raise InvalidResourceException( 282 mutual_tls_auth, 283 "MutualTlsAuthentication must be a map with at least one of the following fields {}.".format( 284 ["TruststoreUri", "TruststoreVersion"] 285 ), 286 ) 287 288 # Create BasepathMappings 289 if self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), string_types): 290 basepaths = [self.domain.get("BasePath")] 291 elif self.domain.get("BasePath") and isinstance(self.domain.get("BasePath"), list): 292 basepaths = self.domain.get("BasePath") 293 else: 294 basepaths = None 295 basepath_resource_list = self._construct_basepath_mappings(basepaths, http_api) 296 297 # Create the Route53 RecordSetGroup resource 298 record_set_group = self._construct_route53_recordsetgroup() 299 300 return domain, basepath_resource_list, record_set_group 301 302 def _construct_route53_recordsetgroup(self): 303 record_set_group = None 304 if self.domain.get("Route53") is not None: 305 route53 = self.domain.get("Route53") 306 if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None: 307 raise InvalidResourceException( 308 self.logical_id, 309 "HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.", 310 ) 311 logical_id = logical_id_generator.LogicalIdGenerator( 312 "", route53.get("HostedZoneId") or route53.get("HostedZoneName") 313 ).gen() 314 record_set_group = Route53RecordSetGroup( 315 "RecordSetGroup" + logical_id, attributes=self.passthrough_resource_attributes 316 ) 317 if "HostedZoneId" in route53: 318 record_set_group.HostedZoneId = route53.get("HostedZoneId") 319 elif "HostedZoneName" in route53: 320 record_set_group.HostedZoneName = route53.get("HostedZoneName") 321 record_set_group.RecordSets = self._construct_record_sets_for_domain(self.domain) 322 323 return record_set_group 324 325 def _construct_basepath_mappings(self, basepaths, http_api): 326 basepath_resource_list = [] 327 328 if basepaths is None: 329 basepath_mapping = ApiGatewayV2ApiMapping( 330 self.logical_id + "ApiMapping", attributes=self.passthrough_resource_attributes 331 ) 332 basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName")) 333 basepath_mapping.ApiId = ref(http_api.logical_id) 334 basepath_mapping.Stage = ref(http_api.logical_id + ".Stage") 335 basepath_resource_list.extend([basepath_mapping]) 336 else: 337 for path in basepaths: 338 # search for invalid characters in the path and raise error if there are 339 invalid_regex = r"[^0-9a-zA-Z\/\-\_]+" 340 if re.search(invalid_regex, path) is not None: 341 raise InvalidResourceException(self.logical_id, "Invalid Basepath name provided.") 342 343 if path == "/": 344 path = "" 345 else: 346 # ignore leading and trailing `/` in the path name 347 m = re.search(r"[a-zA-Z0-9]+[\-\_]?[a-zA-Z0-9]+", path) 348 path = m.string[m.start(0) : m.end(0)] 349 if path is None: 350 raise InvalidResourceException(self.logical_id, "Invalid Basepath name provided.") 351 352 logical_id = "{}{}{}".format(self.logical_id, re.sub(r"[\-\_]+", "", path), "ApiMapping") 353 basepath_mapping = ApiGatewayV2ApiMapping(logical_id, attributes=self.passthrough_resource_attributes) 354 basepath_mapping.DomainName = ref(self.domain.get("ApiDomainName")) 355 basepath_mapping.ApiId = ref(http_api.logical_id) 356 basepath_mapping.Stage = ref(http_api.logical_id + ".Stage") 357 basepath_mapping.ApiMappingKey = path 358 basepath_resource_list.extend([basepath_mapping]) 359 return basepath_resource_list 360 361 def _construct_record_sets_for_domain(self, domain): 362 recordset_list = [] 363 recordset = {} 364 route53 = domain.get("Route53") 365 366 recordset["Name"] = domain.get("DomainName") 367 recordset["Type"] = "A" 368 recordset["AliasTarget"] = self._construct_alias_target(self.domain) 369 recordset_list.extend([recordset]) 370 371 recordset_ipv6 = {} 372 if route53.get("IpV6"): 373 recordset_ipv6["Name"] = domain.get("DomainName") 374 recordset_ipv6["Type"] = "AAAA" 375 recordset_ipv6["AliasTarget"] = self._construct_alias_target(self.domain) 376 recordset_list.extend([recordset_ipv6]) 377 378 return recordset_list 379 380 def _construct_alias_target(self, domain): 381 alias_target = {} 382 route53 = domain.get("Route53") 383 target_health = route53.get("EvaluateTargetHealth") 384 385 if target_health is not None: 386 alias_target["EvaluateTargetHealth"] = target_health 387 if domain.get("EndpointConfiguration") == "REGIONAL": 388 alias_target["HostedZoneId"] = fnGetAtt(self.domain.get("ApiDomainName"), "RegionalHostedZoneId") 389 alias_target["DNSName"] = fnGetAtt(self.domain.get("ApiDomainName"), "RegionalDomainName") 390 else: 391 raise InvalidResourceException( 392 self.logical_id, 393 "Only REGIONAL endpoint is supported on HTTP APIs.", 394 ) 395 return alias_target 396 397 def _add_auth(self): 398 """ 399 Add Auth configuration to the OAS file, if necessary 400 """ 401 if not self.auth: 402 return 403 404 if self.auth and not self.definition_body: 405 raise InvalidResourceException( 406 self.logical_id, "Auth works only with inline OpenApi specified in the 'DefinitionBody' property." 407 ) 408 409 # Make sure keys in the dict are recognized 410 if not all(key in AuthProperties._fields for key in self.auth.keys()): 411 raise InvalidResourceException(self.logical_id, "Invalid value for 'Auth' property") 412 413 if not OpenApiEditor.is_valid(self.definition_body): 414 raise InvalidResourceException( 415 self.logical_id, 416 "Unable to add Auth configuration because 'DefinitionBody' does not contain a valid OpenApi definition.", 417 ) 418 open_api_editor = OpenApiEditor(self.definition_body) 419 auth_properties = AuthProperties(**self.auth) 420 authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer) 421 422 # authorizers is guaranteed to return a value or raise an exception 423 open_api_editor.add_authorizers_security_definitions(authorizers) 424 self._set_default_authorizer( 425 open_api_editor, authorizers, auth_properties.DefaultAuthorizer, auth_properties.Authorizers 426 ) 427 self.definition_body = open_api_editor.openapi 428 429 def _add_tags(self): 430 """ 431 Adds tags to the Http Api, including a default SAM tag. 432 """ 433 if self.tags and not self.definition_body: 434 raise InvalidResourceException( 435 self.logical_id, "Tags works only with inline OpenApi specified in the 'DefinitionBody' property." 436 ) 437 438 if not self.definition_body: 439 return 440 441 if self.tags and not OpenApiEditor.is_valid(self.definition_body): 442 raise InvalidResourceException( 443 self.logical_id, 444 "Unable to add `Tags` because 'DefinitionBody' does not contain a valid OpenApi definition.", 445 ) 446 elif not OpenApiEditor.is_valid(self.definition_body): 447 return 448 449 if not self.tags: 450 self.tags = {} 451 self.tags[HttpApiTagName] = "SAM" 452 453 open_api_editor = OpenApiEditor(self.definition_body) 454 455 # authorizers is guaranteed to return a value or raise an exception 456 open_api_editor.add_tags(self.tags) 457 self.definition_body = open_api_editor.openapi 458 459 def _set_default_authorizer(self, open_api_editor, authorizers, default_authorizer, api_authorizers): 460 """ 461 Sets the default authorizer if one is given in the template 462 :param open_api_editor: editor object that contains the OpenApi definition 463 :param authorizers: authorizer definitions converted from the API auth section 464 :param default_authorizer: name of the default authorizer 465 :param api_authorizers: API auth section authorizer defintions 466 """ 467 if not default_authorizer: 468 return 469 470 if is_intrinsic_no_value(default_authorizer): 471 return 472 473 if is_intrinsic(default_authorizer): 474 raise InvalidResourceException( 475 self.logical_id, 476 "Unable to set DefaultAuthorizer because intrinsic functions are not supported for this field.", 477 ) 478 479 if not authorizers.get(default_authorizer): 480 raise InvalidResourceException( 481 self.logical_id, 482 "Unable to set DefaultAuthorizer because '" 483 + default_authorizer 484 + "' was not defined in 'Authorizers'.", 485 ) 486 487 for path in open_api_editor.iter_on_path(): 488 open_api_editor.set_path_default_authorizer( 489 path, default_authorizer, authorizers=authorizers, api_authorizers=api_authorizers 490 ) 491 492 def _get_authorizers(self, authorizers_config, default_authorizer=None): 493 """ 494 Returns all authorizers for an API as an ApiGatewayV2Authorizer object 495 :param authorizers_config: authorizer configuration from the API Auth section 496 :param default_authorizer: name of the default authorizer 497 """ 498 authorizers = {} 499 500 if not isinstance(authorizers_config, dict): 501 raise InvalidResourceException(self.logical_id, "Authorizers must be a dictionary.") 502 503 for authorizer_name, authorizer in authorizers_config.items(): 504 if not isinstance(authorizer, dict): 505 raise InvalidResourceException( 506 self.logical_id, "Authorizer %s must be a dictionary." % (authorizer_name) 507 ) 508 509 if "OpenIdConnectUrl" in authorizer: 510 raise InvalidResourceException( 511 self.logical_id, 512 "'OpenIdConnectUrl' is no longer a supported property for authorizer '%s'. Please refer to the AWS SAM documentation." 513 % (authorizer_name), 514 ) 515 authorizers[authorizer_name] = ApiGatewayV2Authorizer( 516 api_logical_id=self.logical_id, 517 name=authorizer_name, 518 authorization_scopes=authorizer.get("AuthorizationScopes"), 519 jwt_configuration=authorizer.get("JwtConfiguration"), 520 id_source=authorizer.get("IdentitySource"), 521 function_arn=authorizer.get("FunctionArn"), 522 function_invoke_role=authorizer.get("FunctionInvokeRole"), 523 identity=authorizer.get("Identity"), 524 authorizer_payload_format_version=authorizer.get("AuthorizerPayloadFormatVersion"), 525 enable_simple_responses=authorizer.get("EnableSimpleResponses"), 526 ) 527 return authorizers 528 529 def _construct_body_s3_dict(self): 530 """ 531 Constructs the HttpApi's `BodyS3Location property`, from the SAM Api's DefinitionUri property. 532 :returns: a BodyS3Location dict, containing the S3 Bucket, Key, and Version of the OpenApi definition 533 :rtype: dict 534 """ 535 if isinstance(self.definition_uri, dict): 536 if not self.definition_uri.get("Bucket", None) or not self.definition_uri.get("Key", None): 537 # DefinitionUri is a dictionary but does not contain Bucket or Key property 538 raise InvalidResourceException( 539 self.logical_id, "'DefinitionUri' requires Bucket and Key properties to be specified." 540 ) 541 s3_pointer = self.definition_uri 542 543 else: 544 # DefinitionUri is a string 545 s3_pointer = parse_s3_uri(self.definition_uri) 546 if s3_pointer is None: 547 raise InvalidResourceException( 548 self.logical_id, 549 "'DefinitionUri' is not a valid S3 Uri of the form " 550 "'s3://bucket/key' with optional versionId query parameter.", 551 ) 552 553 body_s3 = {"Bucket": s3_pointer["Bucket"], "Key": s3_pointer["Key"]} 554 if "Version" in s3_pointer: 555 body_s3["Version"] = s3_pointer["Version"] 556 return body_s3 557 558 def _construct_stage(self): 559 """Constructs and returns the ApiGatewayV2 Stage. 560 561 :returns: the Stage to which this SAM Api corresponds 562 :rtype: model.apigatewayv2.ApiGatewayV2Stage 563 """ 564 565 # If there are no special configurations, don't create a stage and use the default 566 if ( 567 not self.stage_name 568 and not self.stage_variables 569 and not self.access_log_settings 570 and not self.default_route_settings 571 and not self.route_settings 572 ): 573 return 574 575 # If StageName is some intrinsic function, then don't prefix the Stage's logical ID 576 # This will NOT create duplicates because we allow only ONE stage per API resource 577 stage_name_prefix = self.stage_name if isinstance(self.stage_name, string_types) else "" 578 if stage_name_prefix.isalnum(): 579 stage_logical_id = self.logical_id + stage_name_prefix + "Stage" 580 elif stage_name_prefix == DefaultStageName: 581 stage_logical_id = self.logical_id + "ApiGatewayDefaultStage" 582 else: 583 generator = logical_id_generator.LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix) 584 stage_logical_id = generator.gen() 585 stage = ApiGatewayV2Stage(stage_logical_id, attributes=self.passthrough_resource_attributes) 586 stage.ApiId = ref(self.logical_id) 587 stage.StageName = self.stage_name 588 stage.StageVariables = self.stage_variables 589 stage.AccessLogSettings = self.access_log_settings 590 stage.DefaultRouteSettings = self.default_route_settings 591 stage.Tags = self.tags 592 stage.AutoDeploy = True 593 stage.RouteSettings = self.route_settings 594 595 return stage 596 597 def _add_description(self): 598 """Add description to DefinitionBody if Description property is set in SAM""" 599 if not self.description: 600 return 601 602 if not self.definition_body: 603 raise InvalidResourceException( 604 self.logical_id, 605 "Description works only with inline OpenApi specified in the 'DefinitionBody' property.", 606 ) 607 if self.definition_body.get("info", {}).get("description"): 608 raise InvalidResourceException( 609 self.logical_id, 610 "Unable to set Description because it is already defined within inline OpenAPI specified in the " 611 "'DefinitionBody' property.", 612 ) 613 614 open_api_editor = OpenApiEditor(self.definition_body) 615 open_api_editor.add_description(self.description) 616 self.definition_body = open_api_editor.openapi 617 618 def to_cloudformation(self): 619 """Generates CloudFormation resources from a SAM HTTP API resource 620 621 :returns: a tuple containing the HttpApi and Stage for an empty Api. 622 :rtype: tuple 623 """ 624 http_api = self._construct_http_api() 625 domain, basepath_mapping, route53 = self._construct_api_domain(http_api) 626 stage = self._construct_stage() 627 628 return http_api, stage, domain, basepath_mapping, route53 629