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