Skip to content

Commit 862fc46

Browse files
authored
Merge pull request #237 from adanaja/morosi-feat
Improve RestApi for stages and aliases
2 parents c198ed0 + b15cba7 commit 862fc46

File tree

6 files changed

+633
-121
lines changed

6 files changed

+633
-121
lines changed

src/e3/aws/troposphere/apigateway/__init__.py

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -170,19 +170,32 @@ def __init__(
170170
path: str,
171171
method_list: list[Method],
172172
resource_list: list[Resource] | None = None,
173+
integration_uri: str | Ref | Sub | None = None,
173174
lambda_arn: str | GetAtt | Ref | None = None,
175+
lambda_arn_permission: str
176+
| GetAtt
177+
| Ref
178+
| dict[str, str | GetAtt | Ref]
179+
| None = None,
174180
) -> None:
175181
"""Initialize a REST API resource.
176182
177183
:param path: the last path segment for this resource
178184
:param method_list: a list of methods accepted on this resource
179185
:param resource_list: a list of child resources
186+
:param integration_uri: URI of a lambda function for this resource
180187
:param lambda_arn: arn of the lambda executed for this resource
188+
:param lambda_arn_permission: lambda arn for which to add InvokeFunction
189+
permission (can be different from the lambda arn executed
190+
by the REST API). A mapping from stage names to lambda arns can
191+
also be passed
181192
"""
182193
self.path = path
183194
self.method_list = method_list
184195
self.resource_list = resource_list
196+
self.integration_uri = integration_uri
185197
self.lambda_arn = lambda_arn
198+
self.lambda_arn_permission = lambda_arn_permission
186199

187200

188201
class Api(Construct):
@@ -224,7 +237,6 @@ def __init__(
224237
:param hosted_zone_id: id of the hosted zone that contains domain_name.
225238
This parameter is required if domain_name is not None
226239
:param stages_config: configurations of the different stages
227-
:param integration_uri: URI of a Lambda function
228240
"""
229241
self.name = name
230242
self.description = description
@@ -895,35 +907,39 @@ def declare_stage(
895907
def _declare_method(
896908
self,
897909
method: Method,
898-
resource: Resource,
899910
resource_id_prefix: str,
900911
resource_path: str,
912+
resource_integration_uri: str | Ref | Sub | None = None,
913+
resource_lambda_arn: str | GetAtt | Ref | None = None,
914+
resource_lambda_arn_permission: str
915+
| GetAtt
916+
| Ref
917+
| dict[str, str | GetAtt | Ref]
918+
| None = None,
901919
) -> list[AWSObject]:
902920
"""Declare a method.
903921
904922
:param method: the method definition
905-
:param resource: resource associated with the method
906923
:param resource_id_prefix: resource_id without trailing Resource
907924
:param resource_path: absolute path to the resource
925+
:param resource_integration_uri: integration URI for the resource
926+
:param resource_lambda_arn: arn of lambda for the resource
927+
:param resource_lambda_arn_permission: lambda arn permission for the resource
908928
:return: a list of AWSObjects to be added to the stack
909929
"""
910930
result = []
911931
id_prefix = name_to_id(f"{resource_id_prefix}-{method.method}")
912932

913-
# Take the global lambda_arn or the one configured for the resource
914-
lambda_arn = (
915-
self.lambda_arn if resource.lambda_arn is None else resource.lambda_arn
916-
)
917-
918-
# Integration URI for the resource
933+
# Take the global integration uri or the one configured for the resource
919934
integration_uri = (
920935
self.integration_uri
921-
if self.integration_uri is not None
922-
else Sub(
923-
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31"
924-
"/functions/${lambdaArn}/invocations",
925-
dict_values={"lambdaArn": lambda_arn},
926-
)
936+
if resource_integration_uri is None
937+
else resource_integration_uri
938+
)
939+
940+
# Take the global lambda arn or the one configured for the resource
941+
lambda_arn = (
942+
self.lambda_arn if resource_lambda_arn is None else resource_lambda_arn
927943
)
928944

929945
integration = apigateway.Integration(
@@ -934,7 +950,13 @@ def _declare_method(
934950
IntegrationHttpMethod="POST",
935951
PassthroughBehavior="NEVER",
936952
Type="AWS_PROXY",
937-
Uri=integration_uri,
953+
Uri=integration_uri
954+
if integration_uri is not None
955+
else Sub(
956+
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31"
957+
"/functions/${lambdaArn}/invocations",
958+
dict_values={"lambdaArn": lambda_arn},
959+
),
938960
)
939961

940962
method_params = {
@@ -952,6 +974,16 @@ def _declare_method(
952974
result.append(apigateway.Method(f"{id_prefix}Method", **method_params))
953975

954976
for config in self.stages_config:
977+
if resource_lambda_arn_permission is not None:
978+
# Use the lambda_arn_permission configured for resource
979+
if isinstance(resource_lambda_arn_permission, dict):
980+
assert (
981+
config.name in resource_lambda_arn_permission
982+
), f"missing lambda arn permission for stage {config.name}"
983+
lambda_arn = resource_lambda_arn_permission[config.name]
984+
else:
985+
lambda_arn = resource_lambda_arn_permission
986+
955987
result.append(
956988
awslambda.Permission(
957989
name_to_id(f"{id_prefix}-{config.name}LambdaPermission"),
@@ -1019,13 +1051,25 @@ def _declare_resources(
10191051
resource_list: list[Resource],
10201052
parent_id_prefix: str | None = None,
10211053
parent_path: str | None = None,
1054+
parent_integration_uri: str | Ref | Sub | None = None,
1055+
parent_lambda_arn: str | GetAtt | Ref | None = None,
1056+
parent_lambda_arn_permission: str
1057+
| GetAtt
1058+
| Ref
1059+
| dict[str, str | GetAtt | Ref]
1060+
| None = None,
10221061
) -> list[AWSObject]:
10231062
"""Create API resources and methods recursively.
10241063
10251064
Each resource can define its own methods and have child resources.
10261065
10271066
:param resource_list: list of resources
10281067
:param parent_id_prefix: id of the parent resource without trailing Resource
1068+
:param parent_path: absolute path to the parent resource
1069+
:param parent_integration_uri: integration URI of the parent resource
1070+
:param parent_lambda_arn: lambda arn of the parent resource
1071+
:param parent_lambda_arn_permission: lambda arn permission of the
1072+
parent resource
10291073
:return: a list of AWSObjects to be added to the stack
10301074
"""
10311075
result: list[AWSObject] = []
@@ -1059,13 +1103,36 @@ def _declare_resources(
10591103

10601104
result.append(resource)
10611105

1106+
# Get the integration URI of this resource.
1107+
# It must be forwarded to children so that they recursively use the
1108+
# same URI
1109+
resource_integration_uri = (
1110+
r.integration_uri
1111+
if r.integration_uri is not None
1112+
else parent_integration_uri
1113+
)
1114+
1115+
# Same for the lambda arn
1116+
resource_lambda_arn = (
1117+
r.lambda_arn if r.lambda_arn is not None else parent_lambda_arn
1118+
)
1119+
1120+
# Same fo the lambda arn permission
1121+
resource_lambda_arn_permission = (
1122+
r.lambda_arn_permission
1123+
if r.lambda_arn_permission is not None
1124+
else parent_lambda_arn_permission
1125+
)
1126+
10621127
# Declare the methods of this resource
10631128
for method in r.method_list:
10641129
result += self._declare_method(
10651130
method=method,
1066-
resource=r,
10671131
resource_id_prefix=resource_id_prefix,
10681132
resource_path=resource_path,
1133+
resource_integration_uri=resource_integration_uri,
1134+
resource_lambda_arn=resource_lambda_arn,
1135+
resource_lambda_arn_permission=resource_lambda_arn_permission,
10691136
)
10701137

10711138
# Declare the children of this resource
@@ -1074,6 +1141,9 @@ def _declare_resources(
10741141
resource_list=r.resource_list,
10751142
parent_id_prefix=resource_id_prefix,
10761143
parent_path=resource_path,
1144+
parent_integration_uri=resource_integration_uri,
1145+
parent_lambda_arn=resource_lambda_arn,
1146+
parent_lambda_arn_permission=resource_lambda_arn_permission,
10771147
)
10781148

10791149
return result

src/e3/aws/troposphere/awslambda/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -509,22 +509,29 @@ def __init__(
509509
description: str,
510510
lambda_arn: str | GetAtt | Ref,
511511
lambda_version: str,
512+
alias_name: str | None = None,
512513
provisioned_concurrency_config: awslambda.ProvisionedConcurrencyConfiguration
513514
| None = None,
514515
routing_config: awslambda.AliasRoutingConfiguration | None = None,
515516
):
516517
"""Initialize an AWS lambda alias.
517518
518-
:param name: function name
519-
:param description: a description of the function
519+
:param name: name of the resource
520+
:param description: a description of the alias
520521
:param lambda_arn: the name of the Lambda function
521522
:param lambda_version: the function version that the alias invokes
523+
:param alias_name: name of the alias. By default the parameter
524+
name will be used as both the name of the resource and the name
525+
of the alias, so this allows for a different alias name. For
526+
example if you have multiple Lambda functions using the same
527+
alias names
522528
:param provisioned_concurrency_config: specifies a provisioned
523529
concurrency configuration for a function's alias
524530
:param routing_config: the routing configuration of the alias
525531
"""
526532
self.name = name
527533
self.description = description
534+
self.alias_name = alias_name
528535
self.lambda_arn = lambda_arn
529536
self.lambda_version = lambda_version
530537
self.provisioned_concurrency_config = provisioned_concurrency_config
@@ -537,7 +544,7 @@ def ref(self) -> Ref:
537544
def resources(self, stack: Stack) -> list[AWSObject]:
538545
"""Return list of AWSObject associated with the construct."""
539546
params = {
540-
"Name": self.name,
547+
"Name": self.alias_name if self.alias_name is not None else self.name,
541548
"Description": self.description,
542549
"FunctionName": self.lambda_arn,
543550
"FunctionVersion": self.lambda_version,
@@ -769,9 +776,11 @@ def create_alias(
769776
:param default_name: default alias name if none is specified
770777
"""
771778
name = config.name if config.name is not None else default_name
779+
id = name_to_id(f"{self.lambda_name}-{name}-alias")
772780
return Alias(
773-
name=name_to_id(f"{self.lambda_name}-{name}-alias"),
781+
name=id,
774782
description=f"{name} alias for {self.lambda_name} lambda",
783+
alias_name=config.name if config.name is not None else id,
775784
lambda_arn=self.lambda_arn,
776785
lambda_version=config.version,
777786
provisioned_concurrency_config=config.provisioned_concurrency_config,

tests/tests_e3_aws/troposphere/apigateway/apigateway_test.py

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -714,29 +714,15 @@ def test_rest_api_nested_resources(stack: Stack, lambda_fun: PyFunction) -> None
714714
stack.s3_bucket = "cfn_bucket"
715715
stack.s3_key = "templates/"
716716

717-
# Lambda for the products resource
718-
products_lambda = PyFunction(
719-
name="productslambda",
720-
description="this is a test",
721-
role="somearn",
722-
code_dir="my_code_dir",
723-
handler="app.main",
724-
runtime="python3.8",
725-
logs_retention_in_days=None,
726-
)
727-
728717
rest_api = RestApi(
729718
name="testapi",
730719
description="this is a test",
731720
lambda_arn=lambda_fun.ref,
732721
resource_list=[
733-
Resource(path="accounts", method_list=[Method("ANY")]),
734722
Resource(
735-
path="products",
736-
# Specific lambda for this resource
737-
lambda_arn=products_lambda.ref,
723+
path="foo",
738724
method_list=[Method("ANY")],
739-
resource_list=[Resource(path="abcd", method_list=[Method("GET")])],
725+
resource_list=[Resource(path="bar", method_list=[Method("GET")])],
740726
),
741727
],
742728
)
@@ -751,3 +737,97 @@ def test_rest_api_nested_resources(stack: Stack, lambda_fun: PyFunction) -> None
751737

752738
print(stack.export()["Resources"])
753739
assert stack.export()["Resources"] == expected
740+
741+
742+
def test_rest_api_multi_lambdas_stages(stack: Stack) -> None:
743+
"""Test REST API with multiple lambdas and stages."""
744+
stack.s3_bucket = "cfn_bucket"
745+
stack.s3_key = "templates/"
746+
747+
# Create two lambdas for two different methods
748+
accounts_lambda, products_lambda = [
749+
PyFunction(
750+
name=f"{name}lambda",
751+
description="this is a test",
752+
role="somearn",
753+
code_dir="my_code_dir",
754+
handler="app.main",
755+
runtime="python3.8",
756+
logs_retention_in_days=None,
757+
)
758+
for name in ("accounts", "products")
759+
]
760+
761+
# Create lambda versions
762+
accounts_lambda_versions, products_lambda_versions = [
763+
AutoVersion(2, lambda_function=lambda_fun)
764+
for lambda_fun in (accounts_lambda, products_lambda)
765+
]
766+
767+
# Create lambda aliases.
768+
# Share the same alias names as it will make it easier to setup the stage
769+
# variable for using the right alias depending on the stage
770+
accounts_lambda_aliases, products_lambda_aliases = [
771+
BlueGreenAliases(
772+
blue_config=BlueGreenAliasConfiguration(
773+
name="Blue", version=lambda_versions.previous.version
774+
),
775+
green_config=BlueGreenAliasConfiguration(
776+
name="Green", version=lambda_versions.latest.version
777+
),
778+
lambda_function=lambda_fun,
779+
)
780+
for lambda_versions, lambda_fun in (
781+
(accounts_lambda_versions, accounts_lambda),
782+
(products_lambda_versions, products_lambda),
783+
)
784+
]
785+
786+
# Create the REST API
787+
rest_api = RestApi(
788+
name="testapi",
789+
description="this is a test",
790+
# Not important as it's overriden in resources
791+
lambda_arn=accounts_lambda.ref,
792+
# Declare prod and beta stages redirecting to correct aliases
793+
stages_config=[
794+
StageConfiguration("default", variables={"lambdaAlias": "Blue"}),
795+
StageConfiguration("beta", variables={"lambdaAlias": "Green"}),
796+
],
797+
# Declare two resources pointing to two different lambdas
798+
resource_list=[
799+
Resource(
800+
path=path,
801+
# Action to invoke the lambda with correct alias
802+
integration_uri="arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/"
803+
"functions/arn:aws:lambda:eu-west-1:123456789012:function:"
804+
f"{lambda_fun.name}:${{stageVariables.lambdaAlias}}/invocations",
805+
# Lambda ARNs for InvokeFunction permissions depending on the stage
806+
lambda_arn_permission={
807+
"default": lambda_aliases.blue.ref,
808+
"beta": lambda_aliases.green.ref,
809+
},
810+
method_list=[Method("ANY")],
811+
)
812+
for path, lambda_fun, lambda_aliases in (
813+
("accounts", accounts_lambda, accounts_lambda_aliases),
814+
("products", products_lambda, products_lambda_aliases),
815+
)
816+
],
817+
)
818+
819+
stack.add(accounts_lambda)
820+
stack.add(products_lambda)
821+
stack.add(accounts_lambda_versions)
822+
stack.add(products_lambda_versions)
823+
stack.add(accounts_lambda_aliases)
824+
stack.add(products_lambda_aliases)
825+
stack.add(rest_api)
826+
827+
with open(
828+
os.path.join(TEST_DIR, "apigatewayv1_test_multi_lambdas_stages.json"),
829+
) as fd:
830+
expected = json.load(fd)
831+
832+
print(stack.export()["Resources"])
833+
assert stack.export()["Resources"] == expected

0 commit comments

Comments
 (0)